Topic 04 / 13

Data Fetching & Caching

~11 min read  //  Next.js Series  //  Coding India

Fetch Where the Data Is Used

Server components fetch directly — no API layer between your page and your data:

export default async function BlogPage() {
  const res = await fetch("https://api.example.com/posts");
  const posts = await res.json();

  return posts.map(p => <PostCard key={p.id} post={p} />);
}

Parallel fetching — start both, await together (same trick as Promise.all in plain JS):

export default async function Dashboard() {
  const [user, stats] = await Promise.all([getUser(), getStats()]);
  ...
}

Duplicate fetch calls with the same URL within one render are automatically deduped — fetch freely in whichever components need the data.

Static vs Dynamic Rendering

Next decides per-route at build time:

  • Static (default): the page is rendered once at build/first-request and served as cached HTML — CDN-fast.
  • Dynamic: rendered per-request — automatic when you use cookies(), headers(), searchParams, or opt out of caching.
// force a route dynamic explicitly if needed
export const dynamic = "force-dynamic";

Caching & Revalidation (ISR)

Control freshness per fetch:

// cache indefinitely (static data)
fetch(url, { cache: "force-cache" });

// never cache (always fresh, makes the route dynamic)
fetch(url, { cache: "no-store" });

// revalidate: cached, refreshed in the background every N seconds
fetch(url, { next: { revalidate: 3600 } });

revalidate is Incremental Static Regeneration — static speed with hourly freshness. The sweet spot for blogs, course catalogues, marketing pages. Route-wide version:

// app/blog/page.js
export const revalidate = 3600;

On-Demand Revalidation

Don’t wait for the timer — bust the cache when content actually changes (e.g. in the server action or webhook that edits it):

import { revalidatePath, revalidateTag } from "next/cache";

// after publishing a post:
revalidatePath("/blog");

// or tag fetches and target them precisely
fetch(url, { next: { tags: ["posts"] } });
revalidateTag("posts");

Databases & ORMs

Direct DB queries (Prisma, Drizzle) aren’t fetch, so they don’t get fetch caching. Either rely on route-level revalidate, or wrap queries:

import { unstable_cache } from "next/cache";

const getCourses = unstable_cache(
  () => db.course.findMany(),
  ["courses"],                                // cache key
  { revalidate: 3600, tags: ["courses"] }
);

Streaming Slow Parts

Wrap a slow component in Suspense so the rest of the page doesn’t wait:

import { Suspense } from "react";

<h1>Dashboard</h1>
<Suspense fallback={<StatsSkeleton />}>
  <SlowStats />        {/* async server component — streams in when ready */}
</Suspense>

Mental model recap: static by default, opt into freshness — and choose revalidation windows per data source, not one global setting.