|$ curl https://forge-ai.dev/api/markdown?path=docs/nextjs/data-fetching
$cat docs/next.js-data-fetching.md
updated Recentlyยท40 min readยทpublished

Next.js Data Fetching

โ—†Next.jsโ—†Data Fetchingโ—†Cachingโ—†Intermediate to Advanced๐ŸŽฏFree Tools
Data Fetching Overview

Next.js provides several data fetching strategies that leverage the server for faster, more secure, and more efficient data loading. In the App Router, you fetch data directly in Server Components using async/await. Combined with built-in caching and revalidation, this eliminates the need for client-side state management for most data needs.

The key principle: fetch data as close to where it is used as possible. Server Components can fetch data at the component level, enabling fine-grained loading states and eliminating waterfall problems through automatic deduplication.

โ„น

info

Server-side data fetching in Next.js is inherently more secure than client-side fetching. Your database credentials, API keys, and tokens never leave the server. Only the rendered result reaches the client.
Fetching in Server Components

Server Components can be async functions. Use async/await to fetch data directly โ€” no useEffect, no useState, no loading state management.

app/products/page.tsx
TSX
1// app/products/page.tsx
2import { db } from "@/lib/database";
3
4export default async function ProductsPage() {
5 // Direct database query โ€” no API endpoint needed
6 const products = await db.product.findMany({
7 orderBy: { createdAt: "desc" },
8 take: 20,
9 });
10
11 return (
12 <div>
13 <h1>Products ({products.length})</h1>
14 <div className="grid grid-cols-3 gap-4">
15 {products.map(product => (
16 <div key={product.id} className="border rounded-lg p-4">
17 <h2 className="font-mono">{product.name}</h2>
18 <p className="text-sm text-gray-500">{product.description}</p>
19 <p className="text-lg font-mono mt-2">${product.price}</p>
20 </div>
21 ))}
22 </div>
23 </div>
24 );
25}

You can also use the native fetch API for external HTTP requests. Next.js extends fetch with caching and revalidation options.

app/posts/page.tsx
TSX
1// app/posts/page.tsx
2
3async function getPosts() {
4 const res = await fetch("https://api.example.com/posts", {
5 // Cache options (extended by Next.js)
6 next: { revalidate: 3600 }, // Revalidate every hour
7 });
8
9 if (!res.ok) throw new Error("Failed to fetch posts");
10 return res.json();
11}
12
13export default async function PostsPage() {
14 const posts = await getPosts();
15
16 return (
17 <div>
18 <h1>Posts</h1>
19 {posts.map(post => (
20 <article key={post.id} className="mb-4">
21 <h2>{post.title}</h2>
22 <p>{post.body}</p>
23 </article>
24 ))}
25 </div>
26 );
27}
Caching Strategies

Next.js provides several caching mechanisms to control how data is stored and refreshed.

Request Memoization

During a single server render, if multiple components call fetch with the same URL and options, the requests are automatically deduplicated. Only one HTTP request is made.

deduplication-example.tsx
TSX
1// These two components make the same fetch
2// Only ONE HTTP request is made during the render
3
4// app/components/user-info.tsx
5async function UserInfo({ userId }: { userId: string }) {
6 const user = await fetch(`https://api.example.com/users/${userId}`).then(r => r.json());
7 return <p>{user.name}</p>;
8}
9
10// app/components/user-avatar.tsx
11async function UserAvatar({ userId }: { userId: string }) {
12 const user = await fetch(`https://api.example.com/users/${userId}`).then(r => r.json());
13 return <img src={user.avatar} alt={user.name} />;
14}

Data Cache (Persistent)

Responses from fetch are cached persistently across requests and server restarts. The cache is stored on the filesystem and can be shared across deployments.

caching-options.tsx
TSX
1// Cached by default (GET requests)
2const res = await fetch("https://api.example.com/data");
3// This response is cached until explicitly revalidated
4
5// Opt out of cache
6const res = await fetch("https://api.example.com/data", {
7 cache: "no-store", // Never cache this response
8});
9
10// Cache with tags for on-demand revalidation
11const res = await fetch("https://api.example.com/data", {
12 next: { tags: ["data", "products"] },
13});

Full Route Cache

During the build, Next.js caches the rendered output of static routes. This means the route is pre-rendered once and served from the CDN on every request. This is the default for routes without dynamic APIs.

app/about/page.tsx
TSX
1// This route is statically cached at build time
2// No dynamic APIs used โ†’ automatic static rendering
3export default async function AboutPage() {
4 const team = await getTeamMembers(); // Fetched at build time
5 return (
6 <div>
7 <h1>About Us</h1>
8 {team.map(member => (
9 <div key={member.id}>{member.name}</div>
10 ))}
11 </div>
12 );
13}

Router Cache (Client-Side)

On the client, Next.js caches prefetched routes and their RSC payloads. This makes client-side navigation nearly instant for previously visited routes.

client-cache.tsx
TSX
1// The Link component prefetches by default
2import Link from "next/link";
3
4// Prefetch is enabled by default (for static routes)
5<Link href="/blog">Blog</Link>
6
7// Disable prefetch for dynamic or rarely-visited routes
8<Link href="/dashboard/analytics" prefetch={false}>
9 Analytics
10</Link>
11
12// router.refresh() invalidates the client cache
13"use client";
14import { useRouter } from "next/navigation";
15
16export function RefreshButton() {
17 const router = useRouter();
18 return <button onClick={() => router.refresh()}>Refresh Data</button>;
19}
Revalidation

Revalidation controls when cached data is refreshed. Next.js supports both time-based (ISR) and on-demand revalidation.

Time-Based Revalidation (ISR)

Set a revalidation interval in seconds. After the interval, the next request will serve the stale response while revalidating in the background. Once the revalidation completes, subsequent requests receive the fresh data.

app/products/page.tsx
TSX
1// app/products/page.tsx
2// Revalidate this route every 60 seconds
3
4export const revalidate = 60;
5
6export default async function ProductsPage() {
7 const products = await fetch("https://api.example.com/products", {
8 next: { revalidate: 60 },
9 }).then(r => r.json());
10
11 return (
12 <div>
13 <h1>Products</h1>
14 <p className="text-xs text-gray-500">
15 Last updated: {new Date().toISOString()}
16 </p>
17 {products.map(product => (
18 <div key={product.id}>{product.name}</div>
19 ))}
20 </div>
21 );
22}

On-Demand Revalidation

Revalidate specific routes or tags when data changes. This is triggered programmatically โ€” typically from a Server Action or API route when content is updated.

app/actions/revalidate.ts
TSX
1// app/actions/revalidate.ts
2"use server";
3
4import { revalidatePath, revalidateTag } from "next/cache";
5
6export async function revalidateProducts() {
7 // Revalidate all routes under /products
8 revalidatePath("/products");
9}
10
11export async function revalidateProduct(productId: string) {
12 // Revalidate a specific product page
13 revalidatePath(`/products/${productId}`);
14}
15
16export async function revalidateAllProducts() {
17 // Revalidate all fetches with the "products" tag
18 revalidateTag("products");
19}
webhook-revalidation.tsx
TSX
1// Tag-based fetch (works with revalidateTag)
2async function getProducts() {
3 const res = await fetch("https://api.example.com/products", {
4 next: { tags: ["products"] },
5 });
6 return res.json();
7}
8
9// API route for webhook-based revalidation
10// app/api/revalidate/route.ts
11import { revalidateTag } from "next/cache";
12import { NextRequest, NextResponse } from "next/server";
13
14export async function POST(request: NextRequest) {
15 const { tag, secret } = await request.json();
16
17 if (secret !== process.env.REVALIDATION_SECRET) {
18 return NextResponse.json({ error: "Invalid secret" }, { status: 401 });
19 }
20
21 revalidateTag(tag);
22 return NextResponse.json({ revalidated: true, now: Date.now() });
23}
โ„น

info

Use tag-based revalidation with webhooks from your CMS or database. When content changes, the CMS sends a webhook to your API route, which calls revalidateTag() to invalidate the relevant cache entries.
Incremental Static Regeneration (ISR)

ISR lets you update static pages after you've built your site. Instead of rebuilding the entire site when content changes, individual pages are regenerated in the background when their revalidation interval expires.

app/blog/[slug]/page.tsx
TSX
1// app/blog/[slug]/page.tsx
2import { db } from "@/lib/database";
3import { notFound } from "next/navigation";
4
5// Generate popular blog posts at build time
6export async function generateStaticParams() {
7 const posts = await db.post.findMany({
8 orderBy: { views: "desc" },
9 take: 100, // Pre-render top 100 posts
10 });
11 return posts.map(post => ({ slug: post.slug }));
12}
13
14// Revalidate each post page individually
15export const revalidate = 3600; // Revalidate every hour
16
17export async function generateMetadata({
18 params,
19}: {
20 params: Promise<{ slug: string }>;
21}) {
22 const { slug } = await params;
23 const post = await db.post.findUnique({ where: { slug } });
24 if (!post) return { title: "Not Found" };
25
26 return {
27 title: post.title,
28 description: post.excerpt,
29 openGraph: { title: post.title, images: [post.coverImage] },
30 };
31}
32
33export default async function BlogPost({
34 params,
35}: {
36 params: Promise<{ slug: string }>;
37}) {
38 const { slug } = await params;
39 const post = await db.post.findUnique({ where: { slug } });
40
41 if (!post) notFound();
42
43 return (
44 <article className="max-w-2xl mx-auto">
45 <h1 className="text-3xl font-mono mb-4">{post.title}</h1>
46 <time className="text-sm text-gray-500">
47 {post.createdAt.toDateString()}
48 </time>
49 <div className="mt-6 prose">{post.content}</div>
50 </article>
51 );
52}
๐Ÿ”ฅ

pro tip

For pages with many dynamic routes, use generateStaticParams to pre-render the most popular pages at build time. Other pages will be rendered on-demand and cached for subsequent requests. This gives you the best of both worlds: fast CDN delivery for popular pages and on-demand generation for long-tail content.
Dynamic APIs

Certain APIs force dynamic rendering. When used, the route segment becomes dynamic โ€” rendered at request time instead of build time.

APIPurposeForce Dynamic
cookies()Read/write cookiesYes
headers()Read request headersYes
searchParamsRead URL query parametersYes
dynamicParamsControl unmatched dynamic segmentsNo (configurable)
app/dashboard/page.tsx
TSX
1// Force dynamic rendering for user-specific pages
2import { cookies, headers } from "next/headers";
3
4export default async function DashboardPage() {
5 const cookieStore = await cookies();
6 const headerStore = await headers();
7
8 const session = cookieStore.get("session");
9 const userAgent = headerStore.get("user-agent");
10
11 if (!session) {
12 return <p>Please log in to access the dashboard.</p>;
13 }
14
15 // This data is fetched at request time, not build time
16 const userData = await getUserData(session.value);
17
18 return (
19 <div>
20 <h1>Welcome, {userData.name}</h1>
21 <p>User Agent: {userAgent}</p>
22 </div>
23 );
24}
route-config.tsx
TSX
1// Explicitly force dynamic for the whole route segment
2export const dynamic = "force-dynamic";
3
4// Or force static
5export const dynamic = "force-static";
6
7// Control unmatched params for dynamic segments
8// app/shop/[...slug]/page.tsx
9export const dynamicParams = true; // true = render on-demand (default)
10// export const dynamicParams = false; // false = 404 for unmatched params
Parallel & Sequential Fetching

How you structure data fetching affects performance. Parallel fetching eliminates waterfalls, while sequential fetching is needed when one request depends on another.

app/dashboard/page.tsx
TSX
1// Parallel fetching โ€” both requests run simultaneously
2export default async function DashboardPage() {
3 // โŒ Sequential โ€” waterfall (slow)
4 const user = await fetchUser(); // 2 seconds
5 const posts = await fetchPosts(); // 2 seconds
6 // Total: 4 seconds
7
8 // โœ… Parallel โ€” no waterfall (fast)
9 const [user, posts] = await Promise.all([
10 fetchUser(), // 2 seconds (runs in parallel)
11 fetchPosts(), // 2 seconds (runs in parallel)
12 ]);
13 // Total: 2 seconds
14
15 return (
16 <div>
17 <UserCard user={user} />
18 <PostList posts={posts} />
19 </div>
20 );
21}
app/orders/[id]/page.tsx
TSX
1// Sequential when dependent
2export default async function OrderPage({
3 params,
4}: {
5 params: Promise<{ id: string }>;
6}) {
7 const { id } = await params;
8
9 // Step 1: Fetch order (need order data to know user)
10 const order = await fetchOrder(id);
11
12 // Step 2: Fetch user (depends on order.userId)
13 const user = await fetchUser(order.userId);
14
15 // Step 3: Fetch recommendations (depends on user preferences)
16 const recommendations = await fetchRecommendations(user.preferences);
17
18 return (
19 <div>
20 <OrderDetails order={order} user={user} />
21 <Recommendations items={recommendations} />
22 </div>
23 );
24}
โ„น

info

Use Promise.all for independent fetches and sequential await for dependent ones. Combined with Request Memoization, this gives you optimal data loading with zero client-side waterfalls.
Streaming Data

Combine Suspense with async Server Components to stream data progressively. Fast components render in the shell immediately while slow components stream in as their data resolves.

app/dashboard/page.tsx
TSX
1// app/dashboard/page.tsx
2import { Suspense } from "react";
3
4async function RevenueChart() {
5 // Takes 3 seconds
6 const data = await fetchRevenueData();
7 return <Chart data={data} />;
8}
9
10async function RecentSales() {
11 // Takes 1 second
12 const sales = await fetchRecentSales();
13 return <SalesList sales={sales} />;
14}
15
16export default function DashboardPage() {
17 return (
18 <div>
19 <h1>Dashboard</h1>
20
21 {/* Fast โ€” appears immediately */}
22 <div className="grid grid-cols-4 gap-4">
23 <StatCard title="Revenue" value="$12,345" />
24 <StatCard title="Orders" value="234" />
25 <StatCard title="Customers" value="1,234" />
26 <StatCard title="Conversion" value="3.2%" />
27 </div>
28
29 {/* Slow โ€” streams in with loading skeleton */}
30 <Suspense fallback={<ChartSkeleton />}>
31 <RevenueChart />
32 </Suspense>
33
34 {/* Medium โ€” streams in independently */}
35 <Suspense fallback={<SalesSkeleton />}>
36 <RecentSales />
37 </Suspense>
38 </div>
39 );
40}
Error Handling

Handle fetch errors gracefully with try/catch in Server Components and error boundaries at the route level.

app/posts/page.tsx
TSX
1// app/posts/page.tsx
2import { Suspense } from "react";
3import { ErrorBoundary } from "@/components/error-boundary";
4
5async function PostList() {
6 try {
7 const posts = await fetch("https://api.example.com/posts", {
8 next: { revalidate: 3600 },
9 });
10
11 if (!posts.ok) {
12 throw new Error(`HTTP ${posts.status}: ${posts.statusText}`);
13 }
14
15 const data = await posts.json();
16 return data.map(post => (
17 <article key={post.id} className="mb-4">
18 <h2>{post.title}</h2>
19 <p>{post.body}</p>
20 </article>
21 ));
22 } catch (error) {
23 return (
24 <div className="p-4 border border-red-500/20 rounded">
25 <p className="text-red-400 text-sm">
26 Failed to load posts. Please try again later.
27 </p>
28 </div>
29 );
30 }
31}
32
33export default function PostsPage() {
34 return (
35 <div>
36 <h1>Posts</h1>
37 <Suspense fallback={<div className="animate-pulse">Loading posts...</div>}>
38 <PostList />
39 </Suspense>
40 </div>
41 );
42}
โš 

warning

Always handle errors in data fetching. A failed fetch in a Server Component without error handling will cause the entire route to fail. Use try/catch for expected failures and error.tsx for unexpected errors.
$Blueprint โ€” Engineering DocumentationยทSection ID: NEXTJS-04ยทRevision: 1.0