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.
🗄️ 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
// 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)
// 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
// 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
// ✅ 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
// 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
// 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
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