Load this skill when designing or implementing API endpoints. Each
rule has a concrete example so reviewers can recognise the shape.
-
All responses are JSON with Content-Type: application/json; charset=utf-8.
-
Success responses include a data key:
{ "data": { "id": "usr_42", "email": "a@b.com" } }
-
Error responses use this exact shape (top-level error object):
{ "error": { "code": "RESOURCE_NOT_FOUND", "message": "User 42 not found" } }
Codes are SCREAMING_SNAKE_CASE strings (machine-readable). The
message is human-readable but does not include stack traces, paths,
or query fragments. See security-checklist#error-handling.
-
Timestamps are ISO 8601 UTC with the Z suffix:
GOOD: "2026-03-26T12:00:00Z"
GOOD: "2026-03-26T12:00:00.123Z"
BAD: "2026-03-26 12:00:00" # not ISO 8601
BAD: "2026-03-26T12:00:00+00:00" # use Z for UTC, not +00:00
BAD: 1711454400 # Unix epoch — pick ISO and stick to it
-
IDs are strings, not integers, in responses. Prefix with a resource
type when the surface has many ID kinds:
{ "data": { "id": "usr_42", "order_id": "ord_17" } }
Rationale: callers serializing IDs through JavaScript can't safely
carry integers beyond 2^53. Strings are unambiguous, future-proof,
and let you switch to ULIDs/UUIDs later without a breaking change.
-
Never return null where [] is more appropriate. A missing list-of-things is empty, not absent.
{ "data": { "user_id": "usr_42", "orders": null } }
{ "data": { "user_id": "usr_42", "orders": [] } }
-
DELETE is idempotent in this project. A DELETE on a resource that does not exist returns 204 No Content, the same response as deleting an extant resource.
DELETE /users/42 → 204 (user existed and was removed)
DELETE /users/42 → 204 (again) (user did not exist; intent satisfied)
Rationale: the client's intent (ensure this resource is absent) is satisfied either way, and idempotent DELETEs play nicely with at-least-once retry policies. This is a deliberate project choice — RFC 9110 allows but does not require it; some APIs (GitHub, Stripe) return 404 instead. Reviewer guidance: BLOCKER 404 from DELETE in code we own; do NOT block when calling third-party APIs that chose otherwise.
-
Don't leak existence via 404 vs 403. When an authenticated user requests a resource they don't have access to, return 404 (not 403). The latter confirms the resource exists; the former doesn't.
if (order.userId !== req.user.id) return res.status(403).end();
const order = await db.orders.findOne({ id: req.params.id, userId: req.user.id });
if (!order) return res.status(404).end();
-
POST is for "create one" — PUT to a known URL is also valid for create. Use PUT /resource/{id} when the client picks the ID (e.g., idempotent provisioning with a pre-allocated UUID); use POST /resource when the server picks the ID.