Data Fetching
In the App Router, data fetching happens primarily in Server Components using native fetch, database clients, or ORMs directly.
Fetching in Server Components
Any Server Component can be async and await data:
// app/posts/page.tsx
async function getPosts() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts');
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: { id: number; title: string }) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
No useEffect, no loading state boilerplate — the page waits for data before rendering.
Fetch Caching
Next.js extends fetch with caching options:
fetch('https://api.example.com/data'); // Cached (default)
fetch('https://api.example.com/data', { next: { revalidate: 60 } }); // ISR — refresh every 60s
fetch('https://api.example.com/data', { cache: 'no-store' }); // Always fresh
| Option | Behavior |
|---|---|
| Default | Cache until manually invalidated |
{ next: { revalidate: N } } |
Revalidate every N seconds |
{ cache: 'no-store' } |
Fetch on every request |
Segment-Level Revalidation
Set revalidation for an entire route segment:
export const revalidate = 3600; // Revalidate every hour
export default async function ProductsPage() {
const products = await getProducts();
return <ProductList products={products} />;
}
Force dynamic rendering: export const dynamic = 'force-dynamic';
Parallel Data Fetching
Fetch multiple resources concurrently:
export default async function DashboardPage() {
const [users, stats] = await Promise.all([getUsers(), getStats()]);
return (
<div>
<Stats data={stats} />
<UserList users={users} />
</div>
);
}
Database Queries
Call your ORM directly in Server Components — no API layer required:
import { db } from '@/lib/db';
export default async function UsersPage() {
const users = await db.user.findMany({ orderBy: { createdAt: 'desc' } });
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name} — {user.email}</li>
))}
</ul>
);
}
Server Actions
Server Actions handle form submissions and mutations on the server:
// app/actions.ts
'use server';
import { revalidatePath } from 'next/cache';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
await db.post.create({ data: { title } });
revalidatePath('/posts');
}
// app/posts/new/page.tsx
import { createPost } from '@/app/actions';
export default function NewPostPage() {
return (
<form action={createPost}>
<input name="title" placeholder="Post title" required />
<button type="submit">Create</button>
</form>
);
}
On-Demand Revalidation
Invalidate cached data after mutations with revalidatePath('/posts') or revalidateTag('posts'). Tag fetches with { next: { tags: ['posts'] } } for targeted invalidation.
Add loading.tsx next to page.tsx for automatic Suspense loading UI.
Next: understand SSR, SSG, and ISR rendering strategies.