Next.js Data Fetching Patterns
Next.js provides multiple patterns for fetching data: Server Components, Route Handlers, Server Actions, and client-side fetching.
Server Component Data Fetching
// app/posts/page.tsx
export default async function PostsPage() {
// Fetch directly in Server Component
const response = await fetch('https://api.example.com/posts', {
cache: 'force-cache' // Default: cache
});
const posts = await response.json();
return (
<div>
{posts.map((post: any) => (
<div key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</div>
))}
</div>
);
}
// With database
import { db } from '@/lib/database';
export default async function UsersPage() {
const users = await db.user.findMany();
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}Caching & Revalidation
// Cache strategies
// 1. Force cache (static generation)
fetch(url, { cache: 'force-cache' });
// 2. No cache (always fresh)
fetch(url, { cache: 'no-store' });
// 3. Revalidate every N seconds
fetch(url, { next: { revalidate: 60 } }); // ISR
// 4. Tag-based revalidation
fetch(url, { next: { tags: ['posts'] } });
// Revalidate by tag
import { revalidateTag } from 'next/cache';
await revalidateTag('posts');
// Revalidate by path
import { revalidatePath } from 'next/cache';
await revalidatePath('/blog');
// Route segment config
export const revalidate = 60; // Revalidate every 60 seconds
export const dynamic = 'force-dynamic'; // Always dynamic
export const dynamic = 'force-static'; // Always static
export const runtime = 'edge'; // Edge runtimeServer Actions
// app/actions.ts
'use server';
import { revalidatePath } from 'next/cache';
import { db } from '@/lib/database';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
// Validate
if (!title || !content) {
return { error: 'Title and content required' };
}
// Save to database
await db.post.create({
data: { title, content }
});
// Revalidate
revalidatePath('/posts');
return { success: true };
}
export async function deletePost(id: string) {
await db.post.delete({ where: { id } });
revalidatePath('/posts');
}
// app/posts/new/page.tsx
import { createPost } from '@/app/actions';
export default function NewPost() {
return (
<form action={createPost}>
<input name="title" required />
<textarea name="content" required />
<button type="submit">Create Post</button>
</form>
);
}
// With Client Component for enhanced UX
'use client';
import { useFormStatus, useFormState } from 'react-dom';
import { createPost } from '@/app/actions';
export default function NewPostForm() {
const [state, formAction] = useFormState(createPost, null);
return (
<form action={formAction}>
<input name="title" />
<textarea name="content" />
{state?.error && <p className="error">{state.error}</p>}
<SubmitButton />
</form>
);
}
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Creating...' : 'Create Post'}
</button>
);
}Parallel Data Fetching
// Sequential (slower)
export default async function Page() {
const user = await fetchUser();
const posts = await fetchPosts(user.id); // Waits for user
return <div>{/* render */}</div>;
}
// Parallel (faster)
export default async function Page() {
const [user, posts] = await Promise.all([
fetchUser(),
fetchPosts()
]);
return <div>{/* render */}</div>;
}
// Streaming with Suspense
import { Suspense } from 'react';
export default function Page() {
return (
<div>
<Suspense fallback={<UserSkeleton />}>
<UserInfo />
</Suspense>
<Suspense fallback={<PostsSkeleton />}>
<PostsList />
</Suspense>
</div>
);
}Keep your own version of these notes — editable, searchable, and organised by your stack.
Start free