Next.js data fetching runs inside async Server Components on the server rather than inside browser useEffect hooks. Because the server executes fetch() or database queries before sending HTML, users see complete content right away and API keys stay hidden.
Next.js extends the standard Web fetch() API with caching and revalidation options. You choose whether each request reads fresh live data, caches statically, or refreshes on a schedule.
flowchart LR
A["Async Server Component"] --> B["fetch(url, options)"]
B --> C{"Cache Option"}
C -- "force-cache" --> D["Cached Response"]
C -- "no-store" --> E["Fresh Per Request"]
C -- "next: { revalidate: 60 }" --> F["Refresh Every 60s"]
Mark a Server Component as async and await your data right inside the component body.
// app/users/page.tsx
interface User {
id: number;
name: string;
email: string;
}
export default async function UsersPage() {
const res = await fetch('https://jsonplaceholder.typicode.com/users', {
next: { revalidate: 300 },
});
if (!res.ok) {
throw new Error('Failed to load users');
}
const users: User[] = await res.json();
return (
<section className="p-6">
<h2 className="text-2xl font-bold mb-4">Team Directory</h2>
<ul className="divide-y divide-gray-200">
{users.map((user) => (
<li key={user.id} className="py-3">
<p className="font-semibold">{user.name}</p>
<p className="text-sm text-gray-500">{user.email}</p>
</li>
))}
</ul>
</section>
);
}
Set caching behavior per request depending on how often your data changes:
// Cache statically until revalidated or rebuilt
await fetch('https://api.example.com/catalog', { cache: 'force-cache' });
// Always fetch live data on every request
await fetch('https://api.example.com/cart', { cache: 'no-store' });
// Refresh cached data in the background every hour
await fetch('https://api.example.com/posts', {
next: { revalidate: 3600, tags: ['posts'] },
});
Call revalidatePath or revalidateTag inside a Server Action right after updating records so users see fresh data immediately.
// app/actions/catalogActions.ts
'use server';
import { revalidatePath, revalidateTag } from 'next/cache';
export async function refreshPosts() {
revalidateTag('posts');
revalidatePath('/users');
}
Promise.all([fetchA(), fetchB()]) so unrelated queries run in parallel instead of creating waterfalls.fetch('/api/users') from a Server Component; call your database or helper function directly.res.ok checks: Always check res.ok before calling res.json() so error HTML pages do not crash JSON parsing.fetch() requests by default?No, starting in Next.js 15, fetch() defaults to uncached (no-store) behavior unless you pass cache: 'force-cache' or next.revalidate.
Yes, Server Components execute only on the server, so you can call Prisma, Drizzle, or SQL drivers directly inside them.
During a single render pass, identical GET fetch() calls with the same URL and options are deduplicated automatically so the server only requests the endpoint once.
Sign in to track your learning journey, earn industry-recognized certificates, and join our elite developer community.
Quick Access With
You've completed this section! Take a quick 5-question quiz to check your understanding.