Data Fetching in Server Components
Fetch data directly inside async Server Components with built-in caching and deduplication.
Search across all documentation pages
Fetch data directly inside async Server Components with built-in caching and deduplication.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Quick-reference recipe card -- copy-paste ready.
// app/posts/page.tsx (Server Component -- the default)
export default async function PostsPage() {
// Cached by default (force-cache)
const res = await fetch("https://api.example.com/posts");
const posts: Post[] = await res.json();
return (
<ul>
{posts.map((p) => (
<li key={p.id}>{p.title}</li>
))}
</ul>
);
}
// Time-based revalidation
await fetch(url, { next: { revalidate: 60 } });
// No caching -- always fresh
await fetch(url, { cache: "no-store" });
// Tag-based revalidation
await fetch(url, { next: { tags: ["posts"] } });When to reach for this: You need to load data for a page or component that has no interactivity -- no useState, no onClick, no browser APIs.
// lib/api.ts
export type Post = { id: number; title: string; body: string };
export async function getPosts(): Promise<Post[]> {
const res = await fetch("https://jsonplaceholder.typicode.com/posts", {
next: { revalidate: 300, tags: ["posts"] },
});
if (!res.ok) throw new Error("Failed to fetch posts");
return res.json();
}
export async function getPost(id: number): Promise<Post> {
const res = await fetch(
`https://jsonplaceholder.typicode.com/posts/${id}`,
{ next: { tags: [`post-${id}`] } }
);
if (!res.ok) throw new Error(`Post ${id} not found`);
return res.json();
}// app/posts/page.tsx
import { getPosts } from "@/lib/api";
import Link from "next/link";
export default async function PostsPage() {
const posts = await getPosts();
return (
<main className="max-w-2xl mx-auto p-6">
<h1 className="text-2xl font-bold mb-4">Posts</h1>
<ul className="space-y-2">
{posts.slice(0, 10).map((post) => (
<li key={post.id}>
<Link
href={`/posts/${post.id}`}
className="text-blue-600 hover:underline"
>
{post.title}
</Link>
</li>
))}
</ul>
</main>
);
}// app/posts/[id]/page.tsx
import { getPost } from "@/lib/api";
import { notFound } from "next/navigation";
type Props = { params: Promise<{ id: string }> };
export default async function PostPage({ params }: Props) {
const { id } = await params;
const numericId = Number(id);
if (Number.isNaN(numericId)) notFound();
const post = await getPost(numericId);
return (
<article className="max-w-2xl mx-auto p-6">
<h1 className="text-3xl font-bold mb-2">{post.title}</h1>
<p className="text-gray-700 leading-relaxed">{post.body}</p>
</article>
);
}What this demonstrates:
useEffect, no client-side loading statelib/ file for reuse across componentsnext.revalidate for time-based ISR and next.tags for on-demand revalidationparams as a Promise (Next.js 15+ pattern)async and call await fetch() directly in the function body.fetch API with cache and next options. The default behavior in Next.js 15+ is cache: "auto", which lets the framework decide based on context.error.tsx boundary.Parallel data fetching (avoid waterfalls):
export default async function DashboardPage() {
// Start both fetches simultaneously
const [users, orders] = await Promise.all([
fetch("https://api.example.com/users").then((r) => r.json()),
fetch("https://api.example.com/orders").then((r) => r.json()),
]);
return (
<>
<UserTable users={users} />
<OrderList orders={orders} />
</>
);
}Non-fetch data sources (database, ORM):
import { cache } from "react";
import { db } from "@/lib/db";
// Wrap with React.cache for request-level memoization
export const getUser = cache(async (id: string) => {
return db.user.findUnique({ where: { id } });
});Passing a promise to a Client Component:
// Server Component
export default async function Page() {
const dataPromise = fetchSlowData(); // do NOT await
return <ClientChart dataPromise={dataPromise} />;
}// Always type your fetch responses
type ApiResponse<T> = { data: T; total: number };
async function getItems(): Promise<ApiResponse<Item[]>> {
const res = await fetch("/api/items");
return res.json();
}
// params is a Promise in Next.js 15+
type PageProps = {
params: Promise<{ slug: string }>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
};Fetch waterfalls -- Sequential await calls create waterfalls where each fetch waits for the previous one. Fix: Use Promise.all() for independent requests or move each fetch into its own Suspense-wrapped component for parallel streaming.
Forgetting error handling -- An uncaught fetch error crashes the entire route. Fix: Throw an error from your fetch helper so the nearest error.tsx boundary catches it, or use try/catch for granular handling.
cache: "no-store" makes the entire route dynamic -- If any fetch in a route uses no-store, the whole route opts out of static generation. Fix: Isolate dynamic fetches into separate components wrapped with Suspense so the rest of the route can remain static.
Non-fetch calls are not deduplicated automatically -- Direct database queries or third-party SDK calls bypass React's request memoization. Fix: Wrap them with React.cache() to get per-request deduplication.
params and searchParams are Promises in Next.js 15+ -- Destructuring them directly without await gives you a Promise object, not the values. Fix: Always await params and await searchParams before accessing properties.
| Alternative | Use When | Don't Use When |
|---|---|---|
| Route Handlers (API routes) | You need a standalone REST endpoint for external consumers | You only need data inside a Server Component |
| SWR or React Query on the client | You need real-time updates, polling, or optimistic mutations | Data can be fetched once on the server |
| Server Actions | You need to mutate data, not read it | You only need GET requests |
React.cache + ORM | You fetch from a database, not an HTTP API | You are calling a public REST API |
unstable_cache (Next.js) | You need Data Cache semantics for non-fetch data sources | Plain fetch with next.revalidate already works |
useEffect or libraries like SWR for async data fetching in Client Componentscache: "no-store" bypasses the Data Cache entirelyrevalidate: 0 also skips caching but signals intent through the ISR APIReact.cache() for non-fetch data sources like direct database queries or ORM callsfetch APIReact.cache() provides per-request memoization for any async function// Next.js 15+: params is a Promise
type Props = { params: Promise<{ id: string }> };
export default async function Page({ params }: Props) {
const { id } = await params;
// use id
}"[object Promise]"await params before accessing properties// Use Promise.all to run fetches in parallel
const [users, orders] = await Promise.all([
fetch("/api/users").then((r) => r.json()),
fetch("/api/orders").then((r) => r.json()),
]);<Suspense>type ApiResponse<T> = { data: T; total: number };
async function getItems(): Promise<ApiResponse<Item[]>> {
const res = await fetch("/api/items");
if (!res.ok) throw new Error("Failed to fetch");
return res.json();
}cache: "auto", which lets the framework decide based on contextforce-cachecache or next.revalidate on every fetch call// Server Component: pass the promise, not the resolved value
export default async function Page() {
const dataPromise = fetchSlowData(); // do NOT await
return <ClientChart dataPromise={dataPromise} />;
}use() hook from Reacterror.tsx boundary catches it if one existsReviewed by Chris St. John·Last updated Jul 19, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥