Headline: In the Next.js App Router, a Server Action is an async function you call directly from a component to run a mutation, and a Route Handler is a standard HTTP endpoint at
app/api/**/route.ts. Reach for a Server Action for mutations driven by your own React UI; reach for a Route Handler for public APIs, webhooks, and streaming. Both compile to publicly callable endpoints, so authenticate inside each one.
Key takeaways
- A Server Action is an async function marked
'use server'that Next.js exposes as an internal POST endpoint you invoke directly from a component — usually a formaction. - A Route Handler is a function exported from
app/api/**/route.tsnamed after an HTTP method that receives a WebRequestand returns a WebResponse. - Use a Server Action for mutations from your own UI — progressive enhancement plus
revalidateTagin the same round trip. - Use a Route Handler for public APIs, webhooks, OAuth callbacks, cacheable GET endpoints, and streaming.
- Both are public endpoints — authenticate, authorize, and validate inputs inside every one.
What is a Server Action vs a Route Handler in Next.js?
A Server Action is an async function marked with the 'use server' directive that Next.js compiles into an RPC-style endpoint. You import it and call it like a normal function — most often as the action of a <form> — and Next.js serializes the call over a POST request behind the scenes.
A Route Handler is a function exported from a route.ts file inside app/, named after the HTTP method it serves (GET, POST, PUT, PATCH, DELETE). It receives a standard Web Request and returns a standard Web Response, so you control the status code, headers, and body directly. It replaces pages/api.
When should I use a Server Action instead of a Route Handler?
Use a Server Action when the mutation is triggered from your own React UI and has no external consumer. A form that updates a profile, a button that deletes a row — these belong in Server Actions because the action lives next to the component that calls it.
Server Actions give you progressive enhancement (a <form action={fn}> works before JS hydrates) and in-place cache revalidation (revalidateTag re-renders affected Server Components in the same round trip).
// app/actions.ts
'use server';
import { revalidateTag } from 'next/cache';
import { auth } from '@/lib/auth';
export async function updateName(formData: FormData) {
const session = await auth(); // authenticate INSIDE the action
if (!session) throw new Error('Unauthorized');
const name = String(formData.get('name') ?? '').trim();
if (!name) return { error: 'Name is required' };
await db.user.update({ where: { id: session.userId }, data: { name } });
revalidateTag('profile');
return { ok: true };
}
When should I use a Route Handler instead of a Server Action?
Use a Route Handler when something outside your React tree calls the endpoint over a stable URL: webhook receivers (Stripe, GitHub), OAuth callbacks, mobile or third-party API consumers, cron jobs, and cacheable GET endpoints.
Route Handlers are also the only option for raw request access or streaming. A Stripe webhook needs the unparsed body to verify the signature; an SSE feed or LLM token stream returns a ReadableStream. Server Actions return a single serialized value.
// app/api/webhooks/stripe/route.ts
import { headers } from 'next/headers';
import { stripe } from '@/lib/stripe';
export async function POST(req: Request) {
const body = await req.text(); // raw body for signature check
const sig = (await headers()).get('stripe-signature')!;
let event;
try {
event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WH_SECRET!);
} catch {
return new Response('Invalid signature', { status: 400 });
}
return Response.json({ received: true });
}
How do Route Handlers and Server Actions compare?
| Aspect | Server Action | Route Handler |
|---|---|---|
| How it's called | Imported and called like a function | HTTP request to a URL |
| HTTP method | Always POST (internally) | Any (GET/POST/PUT/PATCH/DELETE) |
| External clients | No stable public contract | Yes — stable URL + method |
| Progressive enhancement | Yes (form works without JS) | No |
| Cacheable GET | No | Yes (Cache-Control) |
| Streaming | No | Yes (ReadableStream) |
| Cache revalidation |
revalidateTag inline |
Manual |
| Best for | Mutations from your own UI | Public APIs, webhooks, streaming |
Do I need a Route Handler to fetch data for my own page?
No. Inside a Server Component you query your database directly — no Route Handler required. Wrapping the query in app/api and fetching it from the same server just adds an HTTP round trip and a second cache layer for no benefit.
// app/dashboard/page.tsx — no Route Handler, no fetch()
export default async function Page() {
const stats = await db.stats.find();
return <Stats data={stats} />;
}
Reserve Route Handler GETs for data a different client needs: a public JSON API, an edge-cached response, or an endpoint a mobile app calls.
Are Server Actions and Route Handlers both public endpoints?
Yes — both are publicly reachable POST endpoints. A Server Action is not private just because you call it from a component and never see its URL. Next.js compiles each action into an endpoint with an unguessable ID, but that ID can be invoked directly with a crafted request. The action ID is obscurity, not authorization.
Treat every Server Action and Route Handler as an untrusted entry point: authenticate the caller, check authorization, and validate every input inside the function body. The client UI enforces nothing.
FAQ
Q: Can a Server Action handle a GET request?
A: No. Server Actions always execute as POST. For a cacheable GET endpoint, use a Route Handler exporting a GET function.
Q: Should I use a Route Handler to fetch data for my own page?
A: No. In a Server Component, query your data source directly. A Route Handler adds an extra HTTP round trip and cache layer for no benefit.
Q: Are Server Actions secure by default because they have no visible URL?
A: No. Every Server Action compiles to a callable POST endpoint. Authenticate, authorize, and validate inputs inside the action itself.
Q: Which one supports streaming responses?
A: Route Handlers. Return a ReadableStream for SSE or LLM token streams. A Server Action returns one serialized result.
Q: Can a mobile app or third-party service call my Server Actions?
A: Not reliably. Server Action IDs are generated at build time and are not a stable public contract. Expose a Route Handler with a versioned URL.
Originally published on devya.dev. Also on eng-ahmed.com. Built by Devya Solutions.
Top comments (1)
Great comparison. The reminder that both Server Actions and Route Handlers are public entry points is especially important.