single error shapeany failure → RFC 9457 application/problem+json: {type, title, status, detail, code, request_id} (+ errors[] on 422); the client reads the machine code, doesn't parse text; one shape across all routers
business violation → 4xx, not 500a rule violation returns its own code (401 · 403 · 404 · 409 · 422 · 429), doesn't fall through to 500; 500 is only an unplanned failure
code vocabulary mapped401 no identity · 403 no permission · 404 no resource (private masked as 404, not 403) · 409 conflict/lock · 422 body validation · 429 rate limit
time — UTC ISO-8601every datetime in a response serializes as …Z (UTC); neither naive nor local zone; the timezone is applied by the frontend
version in the path/api/v1 · /public/v1; no version → 404; a breaking change → a new version, the old one still lives
identity before the handlera JWT or an ach_… key resolves to user_id → role → ACL before entering the handler; no identity → 401 uniformly on protected routes
rate limit → 429 + Retry-Afterexceeding the window → 429 with a Retry-After header; the body is the same envelope (rate-limit)
no bare routesparameterized over app.routes: every protected route answers 401 without identity — a new endpoint won't slip past the check