Data Fetching & Caching
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.