Verified against Claude Code · 2026-07-28
Design a Route Handler that doesn't leak Express habits into the App Router
Turns an API requirement into a properly structured app/api route.ts — Web-standard Request/Response, an explicit runtime choice, real status codes, and scoped cache invalidation — instead of a ported Express handler.
The prompt
Ready to copy — highlighted parts are example details you can swap.
You are designing one or more Next.js Route Handlers for an API requirement, to be written as app/api/.../route.ts files. These use the Web-standard Request and Response objects, via NextRequest and NextResponse, not an Express-style (req, res) signature — there is no res object anywhere in this code, and nothing here should reach for one.
ENDPOINT PURPOSE
Lets a project admin invite a new team member by email and assign their role
HTTP METHODS NEEDED
POST to create an invite, DELETE to revoke a pending one
REQUEST AND RESPONSE SHAPE
POST body: { email, role }; response: the created invite object including its id and expiresAt. DELETE takes an inviteId route param and returns no body.
RUNTIME CONSTRAINTS
Needs the nodejs runtime — the handler sends a transactional email through a Node-only mailer SDK
EXISTING AUTH PATTERN
Every route in app/api/ reads a verified session via getServerSession() and rejects with 401 if there's no session, and 403 if the session's role isn't admin
DESIGN RULES
Export one named async function per HTTP method actually needed — GET, POST, PATCH, DELETE — never a single catch-all handler that branches internally on request.method; the App Router resolves methods by export name, and a branching catch-all just reimplements what the routing layer already does for you, with worse readability and no benefit. Validate the request body or query parameters against a schema before any business logic runs; on a validation failure, return a 400 with field-level error detail, never a generic 500, and never a 200 with the actual error message buried in the response body where a caller has to parse prose to find out what went wrong. Choose the runtime explicitly with export const runtime set to nodejs or edge, and justify the choice against what the handler actually needs — the edge runtime has no access to Node-only APIs such as fs, most native modules, and some crypto functions, so choose edge only if nothing in this handler's dependency chain genuinely needs those, not by default on the assumption that edge is always faster. If this handler performs a mutation that makes data cached elsewhere in the app stale, call revalidatePath or revalidateTag for the specific affected paths or tags, only after the mutation has actually succeeded, never before, and never a broad revalidation call when a narrowly scoped one would do the identical job with less collateral invalidation. Return real, specific HTTP status codes: 201 on a successful create, 204 with an empty body on a successful delete, 409 on a genuine conflict such as a duplicate unique field, 404 when the target resource genuinely does not exist, rather than 200 for every outcome with the real result encoded only in the body's shape. Apply the existing auth pattern consistently across every exported method in this file, including read-only GET handlers — a Route Handler with an unauthenticated GET sitting next to an authenticated POST on the same resource is a common and easy-to-miss access-control gap that a per-method review can walk right past if auth isn't checked as a cross-cutting rule.
OUTPUT FORMAT
The complete route.ts code. Then a short list covering: which status codes are used and in which specific situation each one fires, which runtime was chosen and the specific reason tied to this handler's actual dependencies, and exactly what gets revalidated after a successful mutation, named by path or tag rather than described vaguely as "the relevant cache."Customize
Optional — swap in your own details for the highlighted parts above.
Why this works
Route Handlers use the standard Web Request/Response objects, not Express's (req, res) pair, and a generic "build me an API endpoint" prompt routinely gets this wrong because a large share of Next.js training data on API routes predates the App Router or was written for the Pages Router's req/res-style handlers — the model has to actively override a strong, plausible-looking pattern it has seen far more often than the correct one. Naming the exact export shape — one async function per HTTP method, resolved by name rather than by branching on request.method — turns a stylistic preference into a structural constraint the model can't quietly drift away from mid-response the way it might drift toward a familiar if/else chain if left unconstrained. Forcing an explicit runtime choice, justified against actual dependencies rather than chosen for perceived speed, stops a specific and expensive mistake: defaulting to edge because it sounds faster and then silently failing on a Node-only dependency at deploy time or, worse, at first real production traffic, rather than catching the mismatch during review where it costs nothing to fix. The revalidatePath/revalidateTag requirement closes a gap that's easy to forget entirely: a Route Handler that mutates data doesn't automatically invalidate anything cached elsewhere in the app — that has to be called explicitly, scoped to the specific path or tag the mutation actually affects, and a model asked only to "handle the POST" has no reason to think about cache invalidation at all unless the prompt makes it part of the definition of a correct mutation handler, not an optional afterthought. Requiring the same auth pattern across every method in the file, including GET, targets a gap that a per-endpoint review tends to miss precisely because it isn't a bug in any single line — a GET handler with no auth check compiles, returns data, and looks correct in isolation, and the actual problem only becomes visible when someone compares it against the POST handler sitting three lines below it in the same file.
What you get back
POST /api/teams/[id]/invites returns 400 with { errors: { email: 'must be a valid email' } } on a bad payload, 201 with the created invite on success, and 409 if an active invite for that email already exists. DELETE /api/teams/[id]/invites/[inviteId] returns 204 with no body on success, 404 if the invite doesn't exist or was already revoked. Runtime: nodejs, because the mailer SDK used to send the invite email has no edge-compatible build. Auth: both methods call the same requireAdmin(session) check used by every other route under app/api/teams/, so a GET added later to this same file would inherit the identical check rather than needing it re-added by hand.
Verified against
Claude Code Sonnet 4.6 · 2026-07-28
GitHub Copilot 2026.7 · 2026-08-03
Changelog
- 2026-07-28 — Initial publish, verified against Claude Code (Sonnet 4.6) and GitHub Copilot on Next.js 16 route handlers.
Need this built into your business?
If a prompt isn't enough — custom software, built and maintained for you — that's Scult's day job.
EXPLORE CUSTOM SOFTWARE
