Next.js Route Handlers vs Server Actions
Use Server Actions for mutations called from your React components. Use Route Handlers when external clients need to call your API.
The pattern is familiar: create a POST endpoint in app/api/, make a fetch() call from the client, handle loading states, parse JSON responses. This is Route Handler mode. But Next.js 13+ introduced Server Actions—a paradigm shift that eliminates boilerplate for internal mutations. The key distinction: Route Handlers are for external APIs; Server Actions are for React components.
Route Handlers: The External API Pattern
Route Handlers (formerly API Routes) live in app/api/[route]/route.ts and define HTTP endpoints. They're the right choice when:
- External clients need to call your backend: Mobile apps, third-party integrations, webhooks
- You need custom HTTP methods:
PATCH,DELETE, non-standard verbs - Response format matters: Streaming responses, custom status codes, non-JSON responses
- Framework-agnostic access: Services that shouldn't depend on Next.js internals
// app/api/users/[id]/route.ts
import { NextResponse } from 'next/server';
export async function DELETE(
request: Request,
{ params }: { params: { id: string } }
) {
await db.users.delete(params.id);
return NextResponse.json({ success: true }, { status: 200 });
}Clients call this endpoint with standard fetch():
// Client component
async function deleteUser(id: string) {
const response = await fetch(`/api/users/${id}`, {
method: 'DELETE'
});
const data = await response.json();
// Handle data...
}This pattern introduces boilerplate: manual fetch(), loading states, error handling, response parsing. For internal React components calling your own backend, this overhead is unnecessary.
Server Actions: The React-First Pattern
Server Actions are functions that execute on the server but can be called directly from React components. They eliminate the HTTP abstraction layer for mutations. Use them when:
- React components need to mutate data: Form submissions, button clicks, user interactions
- You want automatic loading states:
useTransition()for pending UI - Progressive enhancement matters: Forms work without JavaScript
- You're building internal APIs: Only called by your Next.js app
// app/actions.ts
'use server';
import { db } from '@/lib/db';
export async function deleteUser(formData: FormData) {
const id = formData.get('id') as string;
await db.users.delete(id);
revalidatePath('/users');
}Call the Server Action directly from a component:
// Client component
import { deleteUser } from '@/app/actions';
function UserList() {
return (
);
}No fetch(). No manual async/await. No response parsing. Server Actions handle the client-server bridge automatically.
The Performance Difference
Route Handlers incur full HTTP request/response cycle overhead:
- Client makes HTTP request (network latency)
- Request goes through Next.js routing middleware
- Route Handler executes
- Response serialized and sent back
- Client parses response
Server Actions use an optimized path: they're serialized once at build time into internal endpoints that bypass full HTTP routing. For mutations triggered by React components, Server Actions are 10-30% faster due to reduced serialization overhead.
Loading States and Errors
Server Actions integrate with React 18's concurrent features for better UX:
// Client component
import { useTransition } from 'react';
import { deleteUser } from '@/app/actions';
function UserList() {
const [isPending, startTransition] = useTransition();
return (
);
}With Route Handlers, you manage this manually:
// Manual loading state management
const [isDeleting, setIsDeleting] = useState(false);
const [error, setError] = useStateServer Actions reduce 50+ lines of boilerplate for each mutation when you account for loading states, errors, and response handling.
Form Handling: Progressive Enhancement
Server Actions with <form> work without JavaScript—critical for progressive enhancement. If JavaScript fails to load or is blocked, forms still submit using standard browser form submission:
// Works with and without JavaScriptRoute Handlers require JavaScript for all mutations—event.preventDefault(), fetch(), and response handling all depend on client-side JS. This is a progressive enhancement failure.
When to Use Route Handlers
Despite the benefits of Server Actions, Route Handlers remain essential for:
- Webhooks: Stripe, GitHub, external services push data to your endpoints
- Third-party integrations: Zapier, IFTTT, custom API clients
- Streaming responses: Server-Sent Events, real-time data, large file transfers
- Custom authentication: OAuth callbacks, JWT validation, session management
- CORS requirements: External origins need to call your API
// app/api/webhooks/stripe/route.ts
import { NextResponse } from 'next/server';
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(request: Request) {
const signature = request.headers.get('stripe-signature');
const body = await request.text();
const event = stripe.webhooks.constructEvent(
body,
signature!,
process.env.STRIPE_WEBHOOK_SECRET!
);
// Handle webhook event...
return NextResponse.json({ received: true });
}This pattern cannot be replaced by Server Actions—webhooks require standard HTTP endpoints that external systems call.
Common Pitfalls
Pitfall 1: Using Server Actions for external APIs
Mobile apps or third-party services can't call Server Actions directly—they're React-specific. Build Route Handlers for any API surface consumed outside your Next.js app.
Pitfall 2: Ignoring data fetching
Server Actions are optimized for mutations, not data fetching. Use Server Components, fetch() in RSCs, or Route Handlers for GET requests. Server Actions can fetch data, but they're not designed for read-only operations.
Pitfall 3: Missing revalidation
Server Actions don't automatically invalidate cached data. Use revalidatePath() or revalidateTag() to update stale data after mutations:
// app/actions.ts
'use server';
import { revalidatePath } from 'next/cache';
export async function updateUser(formData: FormData) {
const id = formData.get('id') as string;
await db.users.update(id, formData);
revalidatePath('/users'); // Invalidate /users cache
revalidateTag('users'); // Invalidate all users-tagged data
}Decision Framework
Use this quick guide to choose between Route Handlers and Server Actions:
| Use Case | Best Choice | Why |
|---|---|---|
| Form submission from React | Server Action | Progressive enhancement, automatic loading |
| Button click mutation | Server Action | No fetch boilerplate |
| External webhook | Route Handler | Standard HTTP endpoint required |
| Mobile app API | Route Handler | Framework-agnostic access |
| Streaming response | Route Handler | Full HTTP response control |
| Data fetching (GET) | Server Component | Native RSC data fetching |
The rule of thumb: Route Handlers for external APIs, Server Actions for React mutations. Don't default to one or the other—match the tool to the use case.
Advertisement
Explore these curated resources to deepen your understanding
Official Documentation
Tools & Utilities
Further Reading
Server Actions vs Route Handlers: When to Use Each in Next.js
Practical guide comparing both approaches with examples
Next.js 15 Server Actions vs Route Handlers: When to Use Each
Real-world experience migrating from Route Handlers to Server Actions
Server Actions and Forms in Next.js
Official guide to form handling with Server Actions
Related Insights
Explore related edge cases and patterns
Advertisement