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
Modern CSS Replaced Our Layout JavaScript: Container Queries, :has(), and Subgrid in Production
Container queries, :has(), subgrid, and cascade layers let our team delete most of the layout JavaScript we used to ship — ResizeObserver wrappers, parent-class toggles, and height-measuring passes. Here is what each feature replaces, and the two we still treat as progressive enhancement.
Frontend Engineering
AbortController Beyond fetch: Cancellation Patterns We Use in Every React App
Uncancelled fetches do not fail loudly — they resolve late and overwrite fresh state. The AbortController patterns we use in every React app we ship: useEffect cleanup, AbortSignal.timeout(), AbortSignal.any(), and one-signal listener cleanup.