REST API design best practices: URLs, status codes and errors

A good API is predictable, consistent and hard to misuse. These conventions make yours easy to use and easy to evolve.

A clean API with resources, methods and status codes

An API is a contract between your server and the clients that use it. Once others depend on it, changing it is costly. Spending time on a clear design pays off for years.

1. Think in resources, not actions

A resource is a noun: users, orders, products. Use plural nouns in URLs and let the HTTP method be the verb.

Action Request
List orders GET /orders
Get one order GET /orders/42
Create an order POST /orders
Replace an order PUT /orders/42
Update part of an order PATCH /orders/42
Delete an order DELETE /orders/42
List items of an order GET /orders/42/items

Avoid verbs in URLs such as /getOrder or /createUser. For actions that do not fit CRUD, use a clear sub-resource or action path such as POST /orders/42/cancel.

Keep URLs lowercase, consistent and not too deeply nested.

2. Use methods correctly

  • GET reads and must not change data. It is safe and idempotent.
  • POST creates or triggers processing. Not idempotent by default.
  • PUT replaces, PATCH partially updates, DELETE removes. PUT and DELETE are idempotent.

3. Return the right status codes

Situation Code
Success with a body 200 OK
Created 201 Created (with a Location header)
Success, no body 204 No Content
Bad input 400 Bad Request, or 422 for validation errors
Not logged in 401 Unauthorized
Logged in but not allowed 403 Forbidden
Not found 404 Not Found
Conflict (e.g. duplicate) 409 Conflict
Too many requests 429 Too Many Requests (with Retry-After)
Server fault 500, 502, 503

Never return 200 with an error hidden in the body.

4. Consistent error format

Give clients a predictable structure they can handle:

{
  "error": {
    "code": "validation_failed",
    "message": "Email is not valid.",
    "details": [{ "field": "email", "issue": "invalid_format" }],
    "requestId": "a1b2c3"
  }
}

Be helpful but do not leak internals: no stack traces, SQL or server paths. Log those on the server with the request ID.

5. Pagination, filtering and sorting

Never return unbounded lists.

  • Pagination: ?limit=20&offset=40 is simple. Cursor-based (?limit=20&after=abc) is more stable and faster for large or changing data.
  • Filtering: ?status=paid&created_after=2026-01-01.
  • Sorting: ?sort=-created_at.
  • Set a maximum page size.
  • Return paging info (next cursor or total) in the response.

6. Version your API

Plan for change.

  • Put the version in the URL (/v1/orders) or a header.
  • Adding new optional fields is usually safe. Removing or renaming fields, or changing meaning, is a breaking change and needs a new version.
  • Announce deprecations with dates, and give clients time to move.

7. Idempotency for safe retries

Networks fail, and clients retry. For POST requests with side effects (payments, orders), accept an Idempotency-Key header. If the same key arrives again, return the original result instead of creating a duplicate.

8. Security basics

  • Use HTTPS only.
  • Authenticate requests (tokens, API keys, OAuth) and authorise every request against the specific resource. A logged-in user must not read /orders/43 if it belongs to someone else (a very common bug, called BOLA or IDOR).
  • Validate all input: types, lengths, ranges, allowed values.
  • Do not accept fields you did not intend to be set (mass assignment). Use explicit allow-lists.
  • Rate limit and set request size limits.
  • Do not put secrets in URLs, because they end up in logs.
  • Return only the fields clients need. Do not leak other users' data or internal IDs unnecessarily.
  • Configure CORS narrowly if browsers call the API.

9. Make it pleasant to use

  • Consistent naming (pick snake_case or camelCase and keep it).
  • Use ISO 8601 for dates and UTC (2026-11-07T10:00:00Z).
  • Use strings for IDs and for money use integers of the smallest unit (or decimal strings) to avoid floating-point errors.
  • Support conditional requests and caching headers (ETag, Cache-Control) for read-heavy data.
  • Compress responses.

10. Document and test

  • Describe the API with OpenAPI and publish interactive docs.
  • Include examples of requests, responses and errors.
  • Write contract and integration tests.
  • Provide a changelog.

Quick checklist

  • Nouns in URLs, methods as verbs.
  • Correct status codes, consistent errors.
  • Pagination everywhere.
  • Versioning and deprecation plan.
  • Auth on every endpoint, ownership checks, validation.
  • Idempotency for risky POSTs.
  • OpenAPI docs and tests.

Takeaway: model resources, use methods and status codes properly, keep errors and naming consistent, paginate, version, secure every request and document with examples.


Programming roadmaps series, Part 2 (languages and design): 3 of 5. Previous: JavaScript and TypeScript roadmap: from first script to full stack.

Keep reading