apiresthttpbackend

REST API design: the conventions separating a good API from a mess

Resources, verbs, status codes, versioning, pagination and idempotency: practical guide to designing coherent REST APIs others enjoy consuming.

August 26, 2026·9 min read

REST is not a standard with a validator: it's an architectural style every team interprets their way, which is how APIs end up with POST /getUsers coexisting with DELETE /borrar?id=3. The conventions in this article aren't aesthetic whims — they're contracts that halve friction, bugs and support tickets.

Resources, not actions

The guiding principle: your URLs name nouns, HTTP verbs express action.

GET    /articles           ← list
POST   /articles           ← create
GET    /articles/42        ← read one
PUT    /articles/42        ← full replace
PATCH  /articles/42        ← partial update
DELETE /articles/42        ← delete

Compared with the RPC-over-HTTP antipattern (POST /getArticles, POST /deleteArticle?id=42), you gain predictable uniformity: whoever knows one resource knows how to operate it entirely without extra documentation.

Derived rules ending infinite debates:

  • Always plural (/articles, not /article).
  • Hierarchy for nesting, two levels max: /authors/7/articles reads well; /authors/7/articles/42/comments/9/likes asks for root subresources (/likes?comment=9).
  • Filtering and ordering via query string, never paths: /articles?author=7&status=published&sort=-date&page=2.
  • No verbs in routes, no extensions (.json), no capitals.

Status codes: the semantic contract

Returning 200 OK with { "error": "something broke" } breaks clients, caches, monitors and automatic retries. The code is part of the message. The essentials:

Code When
200 OK Success with body (reads, updates)
201 Created Resource created; add Location header of new resource
204 No Content Success without body (deletes, full PUT)
400 Bad Request Malformed/invalid client syntax
401 Unauthorized Not authenticated (misleading name: means "unauthenticated")
403 Forbidden Authenticated but lacking permission
404 Not Found Nonexistent resource — also when hiding existence
409 Conflict State conflict (duplicate email, stale version)
422 Unprocessable Semantically invalid (valid JSON, impossible data)
429 Too Many Requests Rate limit exceeded
500 Your fault, never the client's

The complete system is detailed in our HTTP status codes guide; here just the discipline: 4xx = client's fault (don't retry identically), 5xx = server's fault (retry with backoff makes sense).

Useful errors: the format that actually helps

A well-designed error saves support tickets:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request contains invalid fields",
    "details": [
      { "field": "email", "issue": "invalid format" },
      { "field": "birth_date", "issue": "cannot be in the future" }
    ],
    "request_id": "req_8f3k2"
  }
}

Consistency across ALL endpoints, traceable request_id in logs, messages saying what to fix — never stack traces or internal details (reconnaissance surface).

Idempotency: the property enabling retries

Idempotent = repeating the request yields the same result. GET, PUT and DELETE naturally are; POST isn't (two POSTs = two resources). That's why automatic retries after timeout are safe on some methods and dangerous on others.

For payments and critical operations over POST there's the Idempotency-Key pattern: client generates a unique key per operation and sends it in a header; your server detects repeats and returns the original result instead of double-charging. Stripe popularized it; it should be the mental default for any API moving money or critical state.

Pagination, filters and sorting from day one

Adding pagination after launch breaks consumers. Design first:

GET /articles?page=2&limit=25&sort=-created,title
  • Offset/limit: simple, but skips elements under concurrent inserts.
  • Cursor (?cursor=eyJpZCI6MTAwfQ): stable under concurrent writes, ideal for infinite feeds; opaque to clients.
  • Always return metadata (total, next_cursor) or headers (Link: rel="next").

And cap limit server-side: ?limit=1000000 is a resource attack disguised as query param.

Versioning: decide before needing it

Three strategies, one recommendation:

  • In path (/v1/articles): visible, cacheable, trivially routed. Most common and sufficient for nearly everyone.
  • Header (Accept: application/vnd.myapi.v2+json): purist, invisible in access logs.
  • No version: only if internal and ephemeral.

When launching v2: v1 keeps working untouched (breaking changes = new version), announce with deprecation dates and Sunset header. Silently breaking consumers burns the trust every API needs.

Cross-cutting security

Bearer token auth (OAuth2/OIDC per context — full flow here), mandatory HTTPS no exceptions, rate limiting by key/IP (why and how, combined with properly configured CORS), and edge input validation with strict schemas (Zod, Pydantic): trusting that "the client sends good data" is the front door of half the broken internet.

FAQ

Does GraphQL kill REST? They coexist: GraphQL shines for mobile clients needing exact compositions hating over-fetching; REST remains unbeaten in simplicity, HTTP caching and ecosystem. Many platforms offer both.

Is HATEOAS mandatory to be REST? Purists say yes; practice says no. Mature hyperlinked APIs are rare; nobody invalidates pragmatic REST with good verbs, statuses and resources.

How do I document? OpenAPI/Swagger as source of truth, generated from code or vice versa. Out-of-sync docs are worse than none: automate or drown in tickets.


Look up any response code instantly with our HTTP codes reference, free and right in your browser.

Try it without code

HTTP Status Codes

Complete reference for 1xx-5xx codes.

Open HTTP Status Codes

Built by

Miguel Ángel Colorado Marin (MACM)

Full-Stack Developer · Guadalajara, España

I develop web apps, digital tools and full projects — from design to deployment.

Contact me