Skip to content

A practical checklist for designing REST APIs that survive real teams and real users — versioning, error contracts, pagination, idempotency, and the small choices that decide whether your API ages well or turns into a maintenance burden.

Published
Reading
4 min
Author
Yaroslav Tsarenko

A REST API is a promise. Every field name, every status code, every error message you ship is a promise to future clients — including the ones you have not met yet.

Here are the principles I keep coming back to across every backend I have built as a Full-Stack Developer: e-commerce, logistics, booking flows, CRMs.

1. Resources, not RPC verbs

An API that says POST /createOrder, POST /updateOrder, POST /deleteOrder looks like a bag of functions. An API that says POST /orders, PATCH /orders/{id}, DELETE /orders/{id} looks like a system.

The resource-oriented shape gives you:

  • Predictable URLs.
  • Free semantics from HTTP methods (safe/idempotent/side-effectful).
  • Cache-friendliness on GETs.
  • Less English in your URLs, more nouns.

2. Status codes with meaning

Do not throw everything at 200 and put the real status in the body. Use HTTP:

  • 200 OK — success with a body.
  • 201 Created — resource created; return a Location header.
  • 204 No Content — success with no body.
  • 400 Bad Request — malformed or invalid input.
  • 401 Unauthorized — not authenticated.
  • 403 Forbidden — authenticated but not allowed.
  • 404 Not Found — the resource does not exist for this caller.
  • 409 Conflict — state does not permit the operation (e.g. optimistic concurrency).
  • 422 Unprocessable Entity — well-formed but semantically wrong (validation errors).
  • 429 Too Many Requests — rate-limited.
  • 500 Internal Server Error — you failed.

If your API returns 200 with {"error": "not found"}, every proxy, cache and monitoring tool downstream is now blind.

3. A single error contract

Ship one error envelope, everywhere:

json
{
  "error": {
    "code": "order.not_found",
    "message": "Order 42 does not exist.",
    "details": []
  }
}

Clients pattern-match on code. message is for humans. details carries per-field validation errors when relevant. This one decision saves months of client-side special cases.

4. Versioning: pick one and commit

Two workable options:

  • URL versioning (/v1/orders) — easy, ugly, loud.
  • Header versioning (Accept: application/vnd.acme.v1+json) — clean URLs, harder to debug in a browser.

Do not do "we will version when we need to." You always need to. Do it on day one.

5. Pagination and filtering

  • Prefer cursor pagination (?cursor=...&limit=20) over offset. It scales, it survives insertions, it is boring in a good way.
  • Standardize filter syntax. Pick either ?status=open&status=paid or ?status=open,paid and never mix them.
  • Cap limit. Someone will pass limit=100000.

6. Idempotency keys

Any endpoint that creates or charges should accept an Idempotency-Key header. Store the result under that key for 24 hours. If the same key comes back, return the same response.

This is not exotic. Stripe does it, Shopify does it, and every payment integration I have built lives or dies by it.

7. Async where it belongs

If a request kicks off work that could take longer than 30 seconds, return 202 Accepted with a resource URL the client can poll (or send a webhook):

text
202 Accepted
{
  "job": { "id": "job_123", "status": "queued", "poll_url": "/v1/jobs/job_123" }
}

The endpoint stays fast, the client stays informed, and your workers do the heavy lifting where they belong.

8. Consistency in field naming

Pick snake_case or camelCase and use it everywhere. Do not have created_at next to updatedAt. Do not return user_id in one endpoint and userId in the next. Every inconsistency turns into three lines of adapter code on every client, forever.

9. Rate limiting is a first-class feature

Public APIs need it to protect the platform. Internal APIs need it to protect you from the intern who accidentally writes a for (const id of allIds) fetch(...) loop. Ship a middleware, ship X-RateLimit-* headers, ship 429s.

10. Documentation that ships with the code

Write an OpenAPI spec. Generate the docs. Host them at /docs. Regenerate on every deploy. If your docs live in a wiki that is one release behind, they are worse than no docs.

11. Backwards compatibility rules

  • Never rename a field. Add a new field, deprecate the old, remove it in the next major version.
  • Never change the type of a field.
  • Never turn an optional field into a required one.
  • Adding a new field is always safe.

If you cannot follow these rules, cut a new version.

12. Security defaults

  • HTTPS only. Redirect HTTP.
  • Auth on every endpoint (yes, including "health"). If truly public, mark it explicitly.
  • Validate input on the server. Client validation is UX, not security.
  • Log every 4xx and 5xx with a request ID that comes back in headers.

Closing thought

A great API feels like nothing. Clients build against it, do not think about it, and move on. That "nothing" is the sum of a hundred small decisions made deliberately.

If you have an API that is technically working but feels like an accumulated pile of choices — get in touch. I do this kind of cleanup regularly as a Full-Stack Developer for teams that want their backend to age well.