A REST API is a contract other people build on — so mistakes are expensive to change later. Run this checklist before you ship a new endpoint: it covers the decisions clients feel first, from naming to errors to auth.
Resource naming & structure
- Use nouns, not verbs.
GET /orders, not/getOrders. The HTTP method is the verb. - Plural, consistent collections.
/users,/users/42,/users/42/orders. - Keep nesting shallow — one or two levels; don’t model your whole schema in the URL.
- Match methods to intent — GET reads, POST creates, PUT/PATCH updates, DELETE removes. GET is safe and idempotent.
# collection and member routes
GET /users list users
POST /users create a user
GET /users/42 fetch one
PATCH /users/42 partial update
DELETE /users/42 remove
GET /users/42/orders that user's orders
When an operation genuinely isn’t CRUD — sending an email, kicking off a job — model it as a sub-resource you create (POST /reports) rather than an RPC-style verb like /generateReport. The URL stays a noun; the method carries the action.
Status codes
- 2xx for success — 200 OK, 201 Created (with a
Location), 204 No Content. - 4xx for client errors — 400 bad input, 401 unauthenticated, 403 forbidden, 404 not found, 409 conflict, 422 validation.
- 5xx for server errors — never return 200 with an error body.
Error shapes
- One consistent error format across every endpoint.
- Machine-readable code + human message — e.g.
{ "code": "INVALID_EMAIL", "message": "..." }. - Field-level detail for validation errors so clients can highlight the right input.
- Never leak internals — no stack traces or SQL in responses.
// 422 Unprocessable Entity — the same shape on every endpoint
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Some fields are invalid.",
"fields": [
{ "field": "email", "code": "INVALID_EMAIL", "message": "Enter a valid email." },
{ "field": "age", "code": "OUT_OF_RANGE", "message": "Must be 18 or older." }
],
"requestId": "req_8f3ac21"
}
}
A stable envelope like this lets clients write one error handler instead of special-casing each endpoint. Include a requestId that also appears in your logs — it turns a vague user report into the exact trace, and it’s the one field worth returning even on 5xx responses.
Pagination & filtering
- Paginate list endpoints — cursor-based for large or changing datasets, offset for simple cases.
- Return page metadata — total count and/or next cursor.
- Standardize filtering & sorting via query params, and document allowed fields.
GET /orders?limit=20&cursor=eyJpZCI6MTIzfQ
{
"data": [ /* up to 20 orders */ ],
"page": { "nextCursor": "eyJpZCI6MTQzfQ", "hasMore": true }
}
Pick sane limits — a default page size around 20 and a hard maximum near 100, so no client can ask for a million rows in one call. Cursor pagination stays correct when rows are inserted or deleted mid-scroll; offset pagination (?page=3) is simpler but skips or repeats rows on data that changes between requests.
Versioning
- Version from day one —
/v1/...is simple and cache-friendly. - Only break on a new version — additive changes stay within the current one.
- Give clients a migration window before retiring an old version.
Auth & security
- HTTPS only, always.
- Authenticate every non-public endpoint and enforce authorization per resource.
- Scope tokens to least privilege; keep them short-lived.
- Set CORS deliberately — don’t reflect arbitrary origins.
Authorization: Bearer <access_token>
// pair a short-lived access token with a revocable refresh token
access_token expires in ~15 minutes
refresh_token expires in ~7-30 days, stored securely, revocable
Short access-token lifetimes limit the damage a leaked token can do; the refresh token keeps users signed in without sending long-lived credentials on every request. On a 401 a client refreshes once and retries — on a 403 it doesn’t, because the user is authenticated but simply not allowed to touch that resource.
Validation & limits
- Validate and sanitize all input at the boundary; reject unknown fields.
- Rate-limit public and auth endpoints to prevent abuse.
- Bound payload sizes and paginate to avoid unbounded responses.
- Make writes idempotent where possible (idempotency keys for POST).
Pre-ship checklist
- Nouns, plural, shallow nesting
- Correct status code for every path
- One consistent error shape, no leaked internals
- List endpoints paginated with metadata
- Versioned and documented
- Auth, CORS, rate limits, and input validation in place
FAQ
Should API endpoints use nouns or verbs?
Nouns. Resources are things, and the HTTP method is the verb — GET /orders, POST /orders, DELETE /orders/42. Reserve verb-style endpoints for genuine actions that don't map to a resource.
How should I version a REST API?
Pick one scheme and apply it consistently. URL versioning (/v1/...) is the most common and cache-friendly. Version when you make breaking changes, and keep the old version alive long enough for clients to migrate.
What does a good API error look like?
A consistent JSON shape with a machine-readable code, a human-readable message, and enough detail to act on — returned with the correct HTTP status. Every endpoint should use the same error format.

