Next.js Data Fetching Patterns
โฑ๏ธ ~5-minute bite ยท solve the sandbox to master
5-Year-Old Metaphor
โ The physical, real-world picture. No jargon.๐ฝ๏ธ How you stock the kitchen determines how fresh the meal is.
force-cache
Pantry staples โ buy once, use foreverCached at build time, served from CDN instantly. Perfect for public data that rarely changes: blog posts, marketing pages, product catalogs. Zero runtime cost.
revalidate: 60
Milk with an expiry dateFresh for 60s, then auto-restocked in the background. Users always get something instantly โ at worst it's 60s old. ISR: static performance with eventual freshness.
no-store
Fresh delivery every time you cookEvery request hits the API fresh. No caching at any layer. Use for user-specific, personalized, or highly volatile data: carts, dashboards, live feeds.
tags + revalidateTag
Labeled shelves โ restock only what changedCache with tags; invalidate only the tagged entries. CMS publishes an article โ webhook calls revalidateTag('posts') โ only posts cache is purged. Surgical precision.
Promise.all
Order all dishes simultaneously, not one at a time3 sequential fetches = 300ms. Promise.all = 100ms. Always parallelize independent data fetches. Next.js deduplicates identical fetch() calls within a render automatically.
Build output signals
Interactive Sandbox
โ Move something, see it react instantly.Fetch pattern
Cached forever โ only refreshed on next build/deploy.
| 1 | // Server Component โ cached forever (until next deploy) |
| 2 | async function Page() { |
| 3 | const res = await fetch("https://api.example.com/posts", { |
| 4 | cache: "force-cache", // default in Next.js 13/14 App Router |
| 5 | }); |
| 6 | const posts = await res.json(); |
| 7 | ย |
| 8 | return <PostList posts={posts} />; |
| 9 | } |
| 10 | ย |
| 11 | // Equivalent shorthand (force-cache is the default): |
| 12 | const res = await fetch("https://api.example.com/posts"); |
| 13 | ย |
| 14 | // Build output: |
| 15 | // โ /posts (Static) โ generated once, served from CDN forever |
Staleness clock
Challenge
Which fetch option makes a route dynamically rendered on every request?
Why Should I Care?
โ The exact interview question + the bug it kills.Interview questions
Q: What is ISR (Incremental Static Regeneration)?
Pages are statically generated at build time but re-generated in the background after the revalidate window expires. Users always get cached HTML โ zero server render cost. The first visitor after the window expires still gets the stale page; the next visitor after thatgets the freshly regenerated one. This "stale-while-revalidate" pattern gives static performance with eventual freshness.
| 1 | // next: { revalidate: 60 } on the fetch, OR: |
| 2 | export const revalidate = 60; // route segment config |
| 3 | ย |
| 4 | // Build output: |
| 5 | // โ /blog (ISR) revalidate: 60 |
| 6 | // |
| 7 | // Timeline: |
| 8 | // t=0s : build โ generate HTML |
| 9 | // t=30s : user A โ cached HTML (instant) |
| 10 | // t=61s : user B โ STILL cached (stale, but instant) |
| 11 | // + background regeneration fires |
| 12 | // t=62s : regeneration done โ cache updated |
| 13 | // t=63s : user C โ fresh HTML (instant) |
Q: When should you use on-demand revalidation with tags?
When you know exactly when data changes โ e.g., a CMS webhook fires when an article is published. Instead of polling every N seconds, you call revalidateTag('posts') from a Route Handler the moment new content is available. This gives you always-fresh data without constant unnecessary regeneration โ the cache stays warm until you explicitly bust it.
| 1 | // 1. Tag the fetch in your Server Component: |
| 2 | const res = await fetch("https://cms.example.com/posts", { |
| 3 | next: { tags: ["posts", "homepage"] }, |
| 4 | }); |
| 5 | ย |
| 6 | // 2. Revalidation endpoint (Route Handler): |
| 7 | // app/api/revalidate/route.ts |
| 8 | import { revalidateTag } from "next/cache"; |
| 9 | export async function POST(req: Request) { |
| 10 | const { tag } = await req.json(); |
| 11 | revalidateTag(tag); // only purges "posts" cache entries |
| 12 | return Response.json({ ok: true }); |
| 13 | } |
| 14 | ย |
| 15 | // 3. CMS calls your webhook on publish: |
| 16 | // POST /api/revalidate { "tag": "posts" } |
| 17 | // โ only /blog and pages tagged "posts" regenerate |
| 18 | // โ /products (tagged "products") stays cached |
Q: Why does parallel fetch matter?
Sequential awaits add latency linearly: 3 fetches ร 100ms = 300ms minimum before the page can render. Promise.all fires all fetches concurrently โ total time equals the slowest single fetch (~100ms). On a page with 5 independent data sources averaging 80ms each, the difference is 400ms vs 80ms. Always parallelize when there are no data dependencies between fetches.
| 1 | // โ Sequential: 80 + 100 + 90 + 110 + 70 = 450ms |
| 2 | const user = await fetchUser(); |
| 3 | const orders = await fetchOrders(); |
| 4 | const prefs = await fetchPreferences(); |
| 5 | const notifs = await fetchNotifications(); |
| 6 | const friends = await fetchFriends(); |
| 7 | ย |
| 8 | // โ Parallel: max(80, 100, 90, 110, 70) = 110ms |
| 9 | const [user, orders, prefs, notifs, friends] = await Promise.all([ |
| 10 | fetchUser(), |
| 11 | fetchOrders(), |
| 12 | fetchPreferences(), |
| 13 | fetchNotifications(), |
| 14 | fetchFriends(), |
| 15 | ]); |
| 16 | ย |
| 17 | // Next.js deduplication bonus: identical fetch() URLs |
| 18 | // called by two different Server Components are |
| 19 | // automatically coalesced into one HTTP request. |
The Deep Dive
โ Spec refs, engine internals, the minutiae.Fetch deduplication โ automatic memoization
Next.js extends the native fetch API with automatic request deduplication. If two Server Components in the same render tree call fetch('/api/user') with the same URL and options, only one HTTP request is sent. Both components receive the same response. This means you can write data-fetching logic in each component without worrying about duplicate API calls โ colocate data needs with the component that uses them.
| 1 | // Layout.tsx โ fetches user |
| 2 | async function Layout({ children }: { children: React.ReactNode }) { |
| 3 | const user = await fetch("/api/user").then(r => r.json()); |
| 4 | return <><Header user={user} />{children}</>; |
| 5 | } |
| 6 | ย |
| 7 | // Page.tsx โ also fetches user (same URL) |
| 8 | async function Page() { |
| 9 | const user = await fetch("/api/user").then(r => r.json()); |
| 10 | return <Profile user={user} />; |
| 11 | } |
| 12 | ย |
| 13 | // Result: only ONE HTTP GET /api/user is made. |
| 14 | // Next.js deduplicates per render pass using a Request cache. |
| 15 | // The cache is cleared between requests โ not persistent. |
React.cache โ deduplication for non-fetch data sources
fetch()deduplication is automatic, but what about database queries, filesystem reads, or any async function that isn't fetch? React.cache wraps any function and memoizes its result per render pass โ same arguments, same result, one execution.
| 1 | import { cache } from "react"; |
| 2 | import { db } from "@/lib/db"; |
| 3 | ย |
| 4 | // Wrapped in cache() โ deduplicates across the render tree |
| 5 | export const getUser = cache(async (id: string) => { |
| 6 | return db.user.findUnique({ where: { id } }); |
| 7 | }); |
| 8 | ย |
| 9 | // Now safe to call from multiple Server Components: |
| 10 | // Layout: await getUser(params.id) โ DB query |
| 11 | // Page: await getUser(params.id) โ same result, no second query |
| 12 | // Card: await getUser(params.id) โ same result, no second query |
| 13 | // All three get the same object. One DB round-trip total. |
| 14 | ย |
| 15 | // Unlike fetch(), React.cache() does NOT persist across requests. |
| 16 | // It is scoped to a single server render. |
Route segment config โ route-level overrides
Instead of setting cache options on each individual fetch(), you can configure the entire route segment with a single export. These override per-fetch options.
| 1 | // app/blog/page.tsx |
| 2 | ย |
| 3 | // Revalidate ALL fetches in this route every 60s: |
| 4 | export const revalidate = 60; |
| 5 | ย |
| 6 | // Force dynamic rendering for the entire route: |
| 7 | export const dynamic = "force-dynamic"; |
| 8 | ย |
| 9 | // Force static generation (error if dynamic functions used): |
| 10 | export const dynamic = "force-static"; |
| 11 | ย |
| 12 | // Control behaviour for unknown path params: |
| 13 | export const dynamicParams = true; // (default) generate on-demand |
| 14 | export const dynamicParams = false; // 404 for unknown params |
| 15 | ย |
| 16 | // Specify the runtime: |
| 17 | export const runtime = "edge"; // Edge Runtime (V8, global) |
| 18 | export const runtime = "nodejs"; // Node.js (default) |
| 19 | ย |
| 20 | // Preferred region for edge deployment: |
| 21 | export const preferredRegion = "iad1"; // Washington DC |
revalidateTag vs revalidatePath
revalidateTag(tag)
Invalidates all cache entries that were fetched with this tag โ regardless of which URL or route they appear on.
Use when: data is shared across multiple routes. A "products" tag might appear on /shop, /product/[id], and /homepage simultaneously.
revalidatePath(path)
Invalidates the cached response for a specific route path and all its fetches.
Use when: you know the exact page to update โ e.g., after a user updates their profile, call revalidatePath('/profile') to purge just that route.
| 1 | import { revalidateTag, revalidatePath } from "next/cache"; |
| 2 | ย |
| 3 | // After CMS publishes โ purge all "posts" across all routes: |
| 4 | revalidateTag("posts"); |
| 5 | ย |
| 6 | // After user profile update โ purge only their profile page: |
| 7 | revalidatePath(`/users/${userId}`); |
| 8 | ย |
| 9 | // revalidatePath also accepts a "page" or "layout" type: |
| 10 | revalidatePath("/blog", "layout"); // purge /blog and all children |
| 11 | revalidatePath("/blog/[slug]", "page"); // purge just page, not layout |
Interview Questions
โ Real questions from real interviews โ with answers.force-cache โ responses are cached indefinitely and served statically until revalidation.
The stale cached page โ and their request triggers background regeneration for the next visitor.
revalidateTag when data appears on multiple routes; revalidatePath when you know the exact URL.
searchParams is request-time data โ unknown at build time, so the page cannot be pre-rendered.
fetch() URLs are deduplicated per render pass automatically. Prisma requires React.cache() for the same effect.
Memory Game
โ Quick quiz โ lock the concept in long-term memory.What is the 'data cache' in Next.js App Router and how is it different from the 'full route cache'?