Data Fetching
Learn server-side data fetching, caching, and revalidation strategies
Data Fetching in Server Components
Next.js extends the native fetch API with caching and revalidation options.
In Server Components, you can fetch data directly using async/await without useEffect or
external libraries.
Requests run on the server by default, so you can keep credentials private and send only the rendered HTML to the browser.
🗄️ Caching Options
force-cache - Cache indefinitely (default for static)no-store - Never cache, always freshnext: { revalidate: N } - Revalidate every N secondsnext: { tags: [...] } - Tag-based revalidationBasic Data Fetching
This example shows a server-side helper that fetches posts, applies caching options, and throws an error for failed requests. The page awaits the data and renders it directly.
// app/posts/page.tsx - Server Component
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
// Caching options
cache: 'force-cache', // Default - cache forever
// OR
cache: 'no-store', // Always fetch fresh data
});
if (!res.ok) throw new Error('Failed to fetch posts');
return res.json();
}
export default async function PostsPage() {
const posts = await getPosts();
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
Time-Based Revalidation (ISR)
Use revalidation for content that changes on a schedule. You can set it per request or for the entire page, and control static versus dynamic rendering.
// Revalidate data every 60 seconds
async function getProducts() {
const res = await fetch('https://api.example.com/products', {
next: { revalidate: 60 }, // Seconds
});
return res.json();
}
// Page-level revalidation - affects all fetches
export const revalidate = 60; // Revalidate page every 60 seconds
// Dynamic behavior
export const dynamic = 'force-dynamic'; // Always dynamic
export const dynamic = 'force-static'; // Always static
export const dynamic = 'auto'; // Default - auto detect
On-Demand Revalidation
Cache tags let you revalidate only the data that changed. Trigger invalidation from a Server Action or a webhook-driven Route Handler.
// Tag-based revalidation
async function getPost(slug: string) {
const res = await fetch(`https://api.example.com/posts/${slug}`, {
next: { tags: ['posts', `post-${slug}`] },
});
return res.json();
}
// Revalidate via Server Action or Route Handler
// app/actions.ts
'use server';
import { revalidateTag, revalidatePath } from 'next/cache';
export async function updatePost(slug: string) {
// Update in database...
// Revalidate specific tag
revalidateTag(`post-${slug}`);
// Or revalidate all posts
revalidateTag('posts');
// Or revalidate a specific path
revalidatePath('/posts');
revalidatePath(`/posts/${slug}`);
}
// Route Handler for webhooks
// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache';
import { NextRequest } from 'next/server';
export async function POST(request: NextRequest) {
const { tag, secret } = await request.json();
if (secret !== process.env.REVALIDATION_SECRET) {
return Response.json({ error: 'Invalid secret' }, { status: 401 });
}
revalidateTag(tag);
return Response.json({ revalidated: true });
}
Parallel Data Fetching
Parallel fetching avoids waterfalls. The total wait time becomes the slowest request instead of the sum of all requests.
// ✅ GOOD: Fetch in parallel
async function getData() {
// Start all fetches at the same time
const [users, posts, comments] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/comments').then(r => r.json()),
]);
return { users, posts, comments };
}
// ❌ BAD: Sequential fetching (waterfall)
async function getDataSlow() {
const users = await fetch('/api/users').then(r => r.json());
const posts = await fetch('/api/posts').then(r => r.json());
const comments = await fetch('/api/comments').then(r => r.json());
// Each waits for the previous one!
}
Data Fetching Patterns
Fetch close to where data is rendered to keep components independent and benefit from automatic request deduplication. The preload pattern starts work early to prevent waterfalls.
// Pattern 1: Fetch at component level (recommended)
// Each component fetches its own data
async function UserProfile({ userId }: { userId: string }) {
const user = await getUser(userId);
return <div>{user.name}</div>;
}
async function UserPosts({ userId }: { userId: string }) {
const posts = await getUserPosts(userId);
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}
// Next.js automatically deduplicates identical requests!
// If both components fetch the same user, only 1 request is made
// Pattern 2: Preload pattern for waterfalls
import { preload } from './data';
export default async function Page({ params }: { params: { id: string } }) {
// Start fetching early
preload(params.id);
// Do other work...
// Data is already being fetched
const data = await getData(params.id);
}
Using Databases Directly
Server Components can query databases directly without an API layer. Keep credentials on the server and return only the data you need for rendering.
// In Server Components, access databases directly
// No API routes needed!
import { prisma } from '@/lib/prisma';
import { sql } from '@vercel/postgres';
// Using Prisma
async function getUsers() {
const users = await prisma.user.findMany({
where: { active: true },
include: { posts: true },
});
return users;
}
// Using raw SQL
async function getProducts() {
const { rows } = await sql`
SELECT * FROM products
WHERE stock > 0
ORDER BY created_at DESC
`;
return rows;
}
// Using Drizzle
import { db } from '@/lib/drizzle';
import { users } from '@/lib/schema';
async function getAllUsers() {
return db.select().from(users);
}
Loading States with Suspense
Suspense streams the page in chunks. Fast content renders immediately while slower sections show their own loading states until data arrives.
import { Suspense } from 'react';
// Slow component that fetches data
async function SlowComponent() {
const data = await fetchSlowData(); // Takes 3 seconds
return <div>{data}</div>;
}
// Fast component
async function FastComponent() {
const data = await fetchFastData(); // Takes 100ms
return <div>{data}</div>;
}
// Page with streaming
export default function Page() {
return (
<div>
{/* Shows immediately */}
<h1>Dashboard</h1>
{/* Fast component loads first */}
<Suspense fallback={<p>Loading fast...</p>}>
<FastComponent />
</Suspense>
{/* Slow component streams in later */}
<Suspense fallback={<p>Loading slow...</p>}>
<SlowComponent />
</Suspense>
</div>
);
}
✅ Data Fetching Best Practices
- • Fetch data at the component level, not at the top
- • Use parallel fetching with Promise.all()
- • Leverage automatic request deduplication
- • Use Suspense for loading states
- • Choose appropriate caching strategy per data type
- • Use tags for granular revalidation control