Next.js App Router API routes I trust in production

Table of Contents
Table of Contents
I trust a Next.js App Router API route when I can name the job. Webhook or cron goes in route.ts. Our own form is a Server Action. I do not open a new pages/api file on an app/ tree.
That is the set. Not a second Express app hiding in Next.
I’m Aris Setiawan. I ship Next.js for client work. I already wrote the App Router I keep in `app/` and when I still touch Pages. This post is the API question: which handlers I ship, and which ones I refuse.
The offer page is Hire a Next.js developer. This is the practice, not a route catalog.
What a Next.js App Router API route is
In App Router, an API route is a Route Handler. The file is route.ts under a segment, usually app/api/.../route.ts. You export GET, POST, PUT, PATCH, or DELETE. The function gets the request. It returns a Response.
That is it. No _app. No getServerSideProps. No leftover pages/api handler unless the old tree is still the owner.
I do not treat route.ts as a place to dump every write. If the browser form is ours, a Server Action is the shorter path: it runs on the server, it can revalidate a tag, and I do not have to invent a JSON contract for myself.
A Next.js App Router API route earns its file when something outside our form needs HTTP: a webhook, a cron ping, a mobile client, a tool that posts a signature.
What I open
I pick the file from the job, not from habit.

Route handler (route.ts). External POST. Cron. A signed webhook. A GET that another service will call. The caller is not our React form.
Server Action. The write starts in our UI. Create, update, delete. The action checks the session, does the write, revalidates the tag. If I cannot name the tag, the action is not done.
pages/api. Only when the repo still lives in Pages and that route is already Pages. I still touch it to fix a broken webhook on an old tree. I do not add a new pages/api folder next to a working app/ tree. That is a second request path I have to explain on day two.
If I catch myself opening route.ts because I miss Express, I stop. The product is not a generic API. It is a Next app with a few HTTP edges.
A handler I will ship
A production POST I will leave in the repo looks boring.

I read the body. JSON if I asked for JSON. Raw if the signer needs the raw bytes. I reject junk with a 400 and a named error, not a stack dump.
I check the secret before I write. Webhook signature, cron header, or a session cookie. If the check fails, I return 401 and I do not touch the database.
I do one write. One job. If the handler also sends email, updates two tables, and pings a third service, I split it. after() can finish a side effect once the 200 is safe. The handler should still be explainable in one breath.
I return 200 or 201 with a small JSON body. Then, if the write is ours, I revalidate the tag the list page uses. A webhook that only writes a row and never busts the cache is how a dashboard stays stale until someone hard-refreshes.
Status codes stay boring: 400 bad input, 401 or 403 auth, 404 missing, 409 conflict, 500 I log and I do not leak.
Auth and cache in the same sentence
This is the bug I still inherit. A GET handler reads a session cookie, Next caches it, two users see the same account.

Public GET can cache. A revalidate window or a tag is fine if the data is public and I can name what busts it.
Webhook POST is never cached. I do not add a cache header to look fast.
A route that reads a cookie is dynamic. force-dynamic or no-store. Auth and cache have to be in the same sentence. If I cannot say which, I do not ship the handler.
Our own form write stays a Server Action plus a tag. I do not wrap that write in a route.ts so I can “keep the API consistent.” Consistent with what. The form is the caller.
Edge runtime is a choice, not a default. If I need Node crypto, a file, or a library that is not edge-safe, I stay on Node. I do not flip runtime = 'edge' because a template did.
What I skip
A new pages/api file on an App Router app.
A route.ts for a form that already lives in the same repo. That is a Server Action with extra JSON.
Caching a cookie GET and calling it a performance win.
A handler that returns 200 on a bad signature so the vendor “stops retrying.” That hides a bug you will pay for later.
Business logic in middleware. Middleware stays a redirect, a header, a rewrite. The write stays in the handler or the action.
What I will take
I take the messy case: a webhook that has to verify a signature, a cron that must not run twice, a cookie GET someone cached by accident, an old pages/api that still owns the request. I will also take a clean app/api tree on a new product. I will not take a rewrite that turns every form into a REST surface for no caller.
If you want this owned on your product, that is the work on Hire a Next.js developer. Send the repo or the ticket.
Next in this series is migrating a Pages app to App Router without drama.



