A Next.js App Router app can run server code in middleware.ts, in a route handler at app/api/.../route.ts, and in a Server Action marked with 'use server'. What decides which of the three a given piece of server logic belongs in?
answer
- who calls it, how often
- one operation versus every request
- published URL versus generated endpoint
- middleware cannot render or return props
- actions are your own UI's RPC
basics
~20 sWho calls the code decides. Middleware handles cross-cutting work on every matched request before routing; a route handler is a public HTTP endpoint for callers outside your app; a Server Action is an internal mutation invoked by your own UI.
solid answer
~50 sI ask two questions: who invokes it, and does it apply to every request or to one operation. Middleware runs before Next resolves the request to a route, on whatever its matcher selects, so it suits only cross-cutting envelope work — rewriting, redirecting, setting headers or cookies, coarse gating — and it pays that cost on every matched request. A route handler is a real URL with a method, status and body I control, so it is what I expose to anything that is not my own UI: webhooks, a mobile client, a partner backend, a scheduler. A Server Action is an RPC my own components reference; its endpoint is generated by the build rather than published, so it is an internal mutation surface, not an API. Reads that only my own pages need usually belong in none of the three — they go in the Server Component that renders them.
code
typescript · 8 lines'use server'
import { db } from '@/lib/db'
export async function cancelOrder(orderId: string) {
await db.order.cancel(orderId)
return { ok: true }
}go deeper
Be able to name the three surfaces and say who calls each: middleware for every matched request, a route handler for outside callers, a Server Action for your own UI. Do not confuse an action with a public endpoint.
Expect to justify a placement out loud. Explain that middleware runs before route resolution and only touches the request/response envelope, that a route handler owns a URL and status code, and that an action's endpoint is generated by the build.
Show the production reasoning: the cost middleware adds to every matched request, the attack surface a needless public endpoint creates, and how you keep one rule in a shared module with thin adapters when two callers genuinely exist.
Own the policy. Decide which surfaces the organisation may expose, who owes compatibility to whom once a URL is published, and how you stop middleware from accreting per-feature logic that nobody can safely remove later.
## Three places server code can live In a current App Router Next.js (15 and 16) there are three user-authored places server code runs, and each exists for a different reason. `middleware.ts` sits at the project root (or in `src/`) and exports a `middleware` function. Next runs it for the requests its optional `config` matcher selects, before the request is resolved to a page or a route. It receives a `NextRequest` and returns a `NextResponse`: it can let the request continue, rewrite it to another path, redirect it, or answer it outright, and it can attach headers and cookies along the way. ```ts import { NextResponse, type NextRequest } from 'next/server' export function middleware(request: NextRequest) { return NextResponse.next() } ``` A **route handler** is a `route.ts` file in the `app` directory exporting functions named after HTTP methods (`GET`, `POST`, `PATCH`, `DELETE`, …). It is a URL: anything that knows the path can call it with any HTTP client, and you decide the status code, headers and body. A **Server Action** is an async function carrying the `'use server'` directive. Your own client code references the function and the framework posts to an endpoint it generated, identified by a build-time id. You never write that URL down — it is an artifact of the build, not a documented interface. ## The two questions that actually decide **Who calls it?** If the caller is software you do not ship — a payment provider's webhook, a partner backend, a native mobile app, a cron service, a CLI — it needs a stable path, its own authentication scheme, and a response shape you control. That is a route handler and nothing else will do. If the caller is your own React tree, a Server Action is the smaller thing: no URL to version, no client-side `fetch` to write, no hand-rolled request and response types. **Does it apply to every request, or to one operation?** Middleware is the only surface that runs without the request naming it. That makes it the right home for work that is genuinely cross-cutting — attaching a locale or experiment cookie, rewriting a legacy path, redirecting anonymous traffic away from a whole section, adding response headers — and the wrong home for anything specific to one route, because you would pay for it on every matched request and then re-derive the route yourself from the pathname. ## Why "make everything a route handler" costs something Teams migrating from the Pages Router often reach for `app/api/...` for everything, because that is the shape they know. The costs are real. You publish and must keep versioning a URL for an operation only your own UI performs, which widens the surface anyone on the internet can poke at. You hand-write the client half: the `fetch` call, the JSON encoding, the error mapping, the loading state plumbing. And you get two definitions of the same payload — one in the handler, one in the component — that drift. For a page's own reads the cost is worse: a Server Component can query the data layer directly, so routing that read through your own HTTP endpoint adds a network hop and a serialization step for nothing. ## Why "do it in middleware" costs something Middleware looks attractive because it sees everything, and that is exactly the problem. It sits on the critical path of every request its matcher selects, so any work there is multiplied by traffic that does not need it. It also has no channel into the component tree — it cannot return an object to the page — so anything it computes has to be squeezed into headers, cookies or a rewritten URL and parsed back out, losing types on the way. Anything that needs the resolved route, the rendered data, or a real response body is better placed downstream. ## The shape most applications settle into - Page reads happen in Server Components, colocated with the markup that needs them. - Mutations triggered by your own UI are Server Actions. - Anything a non-browser caller touches is a route handler with its own authentication. - Middleware stays thin: a short list of decisions that must happen before a route is chosen. When two of them look equally plausible, the tie-break that holds up in an interview is ownership of the contract. A route handler's contract belongs to its callers, so it must stay stable; a Server Action's contract belongs to the same deployment that calls it, so it can change freely with the UI. Choosing the surface is really choosing who you owe compatibility to.
- Your page needs data from your own database. Why is a route handler usually the wrong way to get it?Because a Server Component already runs on the server and can call the data layer directly. Going through your own HTTP endpoint adds a network hop, a JSON round trip and a second copy of the payload type, and it exposes an internet-reachable URL for a read that only your renderer performs. Route handlers earn their keep when a caller outside the render actually needs them.
- Is there a case where the same logic legitimately needs both a route handler and a Server Action?Yes — when one operation has two kinds of caller: your own form and an external system. The answer is not to duplicate it. Put the rule in a plain server module and make each surface a thin adapter that authenticates its own caller, validates its own input, calls the shared function and shapes its own response.
- Where do middleware's limits push work downstream?Middleware cannot render, cannot return an object to the component tree, and cannot see the resolved route's data. Anything it computes must travel as a header, a cookie or a rewritten URL. So it holds decisions — continue, rewrite, redirect, stamp a header — while anything that produces content or depends on route data belongs in the route or the component.
Middleware is the doorman who checks everyone entering the building, a route handler is the public reception desk with a listed phone number, and a Server Action is an intercom line wired only between your own rooms.
saying these in an interview costs you the question
- Server Actions are just public POST endpoints anyone can call
- Middleware is where you put your API endpoints
- Pages must fetch their data through your own route handlers
- Middleware runs after the page renders
- A Server Action's generated id is a URL you can publish