Overview
Understand how data flows between client and server in Next.js. Use Server Components for reads, Server Actions for mutations, and Route Handlers for external APIs.
What it is
Next.js data patterns: Server Components fetch data directly, Server Actions handle mutations with revalidation, Route Handlers serve external API consumers.
Why we use it
Clear separation between reads and writes, automatic cache invalidation, and secure server-side execution.
When to use
Server Components for page data, Server Actions for form submissions and mutations, Route Handlers for mobile apps or external integrations.
Key Features
- Server Components for direct data fetching
- Server Actions for authenticated mutations
- Route Handlers for external API
- Automatic cache revalidation after mutations
Data Flow Architecture
// Data Flow Architecture // // ┌──────────────────────────────────────────────────────────────────┐ // │ CLIENT (Browser) │ // ├──────────────────────────────────────────────────────────────────┤ // │ │ // │ ┌─────────────────┐ ┌─────────────────┐ │ // │ │ Client Component │ ─────▶ │ Server Action │ │ // │ │ (onClick) │ │ (mutations) │ │ // │ └─────────────────┘ └────────┬────────┘ │ // │ │ │ // ├────────────────────────────────────────┼─────────────────────────┤ // │ SERVER │ │ // ├────────────────────────────────────────┼─────────────────────────┤ // │ ▼ │ // │ ┌─────────────────┐ ┌─────────────────┐ │ // │ │ Server Component │ ◀───── │ Service │ │ // │ │ (data fetch) │ │ (business) │ │ // │ └─────────────────┘ └────────┬────────┘ │ // │ │ │ // │ ┌────────▼────────┐ │ // │ │ Repository │ │ // │ │ (Prisma) │ │ // │ └────────┬────────┘ │ // │ │ │ // └────────────────────────────────────────┼─────────────────────────┘ // ▼ // ┌───────────┐ // │ Database │ // └───────────┘
Quick Start
Server Component
Fetch data directly in a Server Component.
// Server Component (data fetching)
// Runs on server, returns HTML
export default async function ProjectPage({ params }: Props) {
// Direct database access - no API call needed
const project = await getProject(params.id);
return (
<div>
<h1>{project.name}</h1>
<ProjectDetails project={project} />
</div>
);
}Patterns
Server Actions
Handle form submissions with revalidation.
// Server Action (mutations)
// src/features/projects/actions.ts
'use server';
import { revalidatePath } from 'next/cache';
import { updateProject } from './services';
export async function updateProjectAction(id: string, data: UpdateData) {
const session = await auth();
if (!session) throw new AuthenticationError();
const result = await updateProject(id, data);
// Revalidate the page to show updated data
revalidatePath(`/projects/${id}`);
return result;
}Route Handlers
Create API endpoints for external consumers.
// Route Handler (external API)
// src/app/api/v1/projects/[id]/route.ts
import { validateRequest } from '@/lib/validation';
import { updateProject } from '@/features/projects';
export async function PATCH(
request: Request,
{ params }: { params: { id: string } }
) {
const session = await auth();
if (!session) throw new AuthenticationError();
const validation = validateRequest(await request.json(), UpdateSchema);
if (!validation.success) return validation.error;
const result = await updateProject(params.id, validation.data);
return NextResponse.json({ ok: true, data: result });
}Revalidation
Invalidate cache after data mutations.
// Revalidation patterns
// 1. Path revalidation (most common)
import { revalidatePath } from 'next/cache';
revalidatePath('/projects'); // Revalidate all projects
revalidatePath(`/projects/${id}`); // Revalidate specific project
// 2. Tag revalidation (for related data)
import { revalidateTag } from 'next/cache';
revalidateTag('projects');
// 3. In fetch with tags
const data = await fetch('/api/projects', {
next: { tags: ['projects'] }
});Watch Out
Importing database code in client components
Don't
// Client importing database code
'use client';
import { prisma } from '@/lib/db'; // ❌ NEVER!
export function ClientComponent() {
// Prisma doesn't work in browser!
const data = await prisma.user.findMany();
}Do
// Server Component handles data
// This runs on server, not in browser
export default async function UsersPage() {
// ✅ Server-side data fetch
const users = await getUsers();
return <UserList users={users} />;
}
// Or use Server Action from client
'use client';
import { fetchUsers } from './actions';
export function UserSearch() {
const loadUsers = async () => {
const users = await fetchUsers(); // ✅ Server Action
};
}Forgetting to revalidate after mutations
Don't
// Mutation without revalidation
'use server';
export async function deleteProject(id: string) {
await projectRepository.delete(id);
// Page still shows deleted project! ❌
}Do
// Mutation with revalidation
'use server';
export async function deleteProject(id: string) {
await projectRepository.delete(id);
// Revalidate to remove from list ✅
revalidatePath('/projects');
return { ok: true };
}- Excessive prop drilling instead of co-locating data
- Fetching more data than needed