11

Scaffolding the API layer, I kept getting route handlers that parsed await req.json() with zero validation, returned bare strings on error, and used a different error shape in every file, so the frontend couldn't handle failures generically.

The prompt below makes every route validate input with zod, return one canonical error envelope, and set correct status codes, and it forces a tiny wrapper so I'm not repeating the try/catch boilerplate in twelve files. The frontend now has exactly one error shape to handle.

Open question: do you version the error envelope from day one or add it later?

THE PROMPT
Scaffold the API route handlers for a Next.js 14 App Router app (app/api/**/route.ts). Consistency and validation are the point.

SHARED FIRST: create a `withRoute` wrapper and canonical response helpers.
- Success envelope: `{ ok: true, data: T }`. Error envelope: `{ ok: false, error: { code: string, message: string, details?: unknown } }`. Every route returns one of these two shapes, always.
- `withRoute` handles: JSON parse, zod validation of body/query/params, method checking, and a single try/catch that maps thrown errors to the error envelope with the right status. No route repeats this boilerplate.
- Status codes: 400 validation, 401 unauth, 403 forbidden, 404 not found, 409 conflict, 422 semantic, 500 unexpected. Map error classes to these; never return 200 with an error body.

PER ROUTE:
- Validate input with a zod schema before touching any logic. Reject unknown fields (.strict()).
- Type the handler so `data` is the parsed, typed input, not `any`.
- Never leak internal error messages or stack traces to the client on 500; log server-side, return a generic message + a code.

DELIVER: lib/api/withRoute.ts, lib/api/responses.ts, and two example routes (one GET with query params, one POST with a body). Strict TS, must build clean. End by showing the exact JSON a validation failure returns.

2 Answers

12

Canonical envelope from day one, yes, retrofitting it means touching every route and every call site later. On the wrapper: make sure the error code is a stable machine-readable enum, not a humanized string, because the frontend will switch on it. Humans read message, code switches on code. I also add a requestId to the error envelope so a user can paste it and I can grep the logs, that pays for itself the first time prod breaks.

THE PROMPT
Add a stable `code` enum (e.g. VALIDATION_ERROR, NOT_FOUND, CONFLICT) the frontend can switch on, and include a `requestId` in every error envelope for log correlation.
3

The .strict() on input is doing quiet security work here, without it, mass-assignment sneaks extra fields into your create handlers and someone sets role: admin on signup. I'd make that a named rule, not a parenthetical. Also worth having withRoute enforce that the response actually matches the envelope type at the boundary, so a route can't accidentally return a raw object and bypass the whole contract.

Your Answer