Route Handlers vs Server Actions in Next.js: When Our Team Reaches for Each
Table of Contents
About the Author

Ahmed Mahmoud
Author & Developer
Software engineer passionate about web development and user experience design.
Founder of Devya · eng-ahmed.com ↗Key takeaways
A Server Action is an async function marked `'use server'` that Next.js exposes as an internal POST endpoint we invoke directly from a Server or Client Component — usually a form action.
A Route Handler is a function exported from `app/api/**/route.ts` named after an HTTP method (GET, POST, and so on) that receives a Web Request and returns a Web Response.
We use a Server Action for mutations triggered from our own UI — it gives progressive enhancement and lets us call `revalidatePath`/`revalidateTag` in the same round trip.
We use a Route Handler for public APIs, webhooks, OAuth callbacks, cacheable GET endpoints, and streaming — anything a non-React client consumes over a stable URL.
Both are public endpoints. A Server Action is not private just because it has no visible URL; we authenticate, authorize, and validate inputs inside every action and handler.
Neither is needed to read data inside a Server Component — we query the data source directly and skip the extra HTTP hop.
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. We 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 we control the status code, headers, and body directly. It is the App Router replacement for `pages/api`.
When should we use a Server Action instead of a Route Handler?
We use a Server Action when the mutation is triggered from our own React UI and has no external consumer. A form that updates a profile, a button that deletes a row, a like toggle — these belong in Server Actions because the action lives next to the component that calls it and needs no public contract.
Server Actions give us two things a Route Handler does not: progressive enhancement, so a form submits and works even before JavaScript hydrates; and in-place cache revalidation, so calling `revalidateTag('profile')` at the end of the action re-renders the affected Server Components in the same round trip with no manual client refetch.
When should we use a Route Handler instead of a Server Action?
We use a Route Handler when something outside our own React tree needs to call the endpoint over a stable URL. Webhook receivers such as Stripe and GitHub, OAuth callbacks, mobile-app or third-party API consumers, cron jobs, and any GET endpoint we want cached at the CDN all require the explicit HTTP contract only a Route Handler gives.
Route Handlers are also the only option when we need raw request access or streaming. A Stripe webhook needs the unparsed request body to verify the signature; a Server-Sent Events feed or an LLM token stream needs to return a ReadableStream. Server Actions return a single serialized value and cannot do either.
How do Route Handlers and Server Actions compare?
How it is called: a Server Action is imported and called like a function; a Route Handler is invoked by an HTTP request to a URL.
HTTP method: a Server Action always runs as POST internally; a Route Handler can serve any method (GET, POST, PUT, PATCH, DELETE).
External clients: a Server Action has no stable public contract; a Route Handler exposes a stable URL and method for non-React clients.
Progressive enhancement: a Server Action form works without JavaScript; a Route Handler does not offer it.
Streaming and cacheable GET: a Route Handler supports both (ReadableStream and Cache-Control); a Server Action supports neither.
Best for: Server Actions fit mutations from our own UI; Route Handlers fit public APIs, webhooks, and streaming.
Do we need a Route Handler to fetch data for our own page?
No. Inside a Server Component we query the database or call the service 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.
We reserve Route Handler GET endpoints for data that a different client needs: a public JSON API, a response we want cached at the edge with Cache-Control, or an endpoint a mobile app calls.
Are Server Actions and Route Handlers both public endpoints?
Yes — both are publicly reachable POST endpoints, and this is the mistake we see most. A Server Action is not private just because it is called from a component and never shows 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.
We treat every Server Action and every Route Handler as an untrusted entry point: authenticate the caller, check they are allowed to perform the operation, and validate every input inside the function body. The client-side UI enforces nothing — a hidden form field or a disabled button stops no one.
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 we use a Route Handler to fetch data for our own page?
A: No. In a Server Component, query the data source directly. A Route Handler adds an extra HTTP round trip and its own cache layer for no benefit when the page is already server-rendered.
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; the generated ID is not a security boundary.
Q: Which one supports streaming responses?
A: Route Handlers. Return a ReadableStream for Server-Sent Events or LLM token streams. A Server Action returns one serialized result and cannot stream.
Q: Can a mobile app or third-party service call our 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 for any external consumer.
Further Reading
Frontend Engineering
URL State in the Next.js App Router: Type-Safe Search Params That Survive a Refresh
We move filters, tabs, and pagination out of useState and into the URL query string, so a refresh or a shared link restores the exact view. Here is how our team reads and writes search params in the Next.js App Router, and when nuqs earns its place.
Frontend Engineering
Error Handling in the Next.js App Router: What Each Error File Actually Catches
In the Next.js App Router, error.tsx defines an error boundary that catches render errors in a route segment and its children — but not its own layout, and not event-handler errors. Our engineering notes on error.tsx, global-error.tsx, not-found.tsx, the production digest, and the reset function.