REST API design done right

Checklist 10 min read Updated August 2026

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.

Practice this
Build real APIs with Node & Express
Design endpoints, handle errors, and wire auth in a full IDE — with automated grading.
Practice backend

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.

Put it into practice

Solve real challenges in a full browser IDE — graded automatically.

Browse frameworks