Backend · August 14, 2026 · 9 min read

REST API Design: Decisions That Age Well

Resource naming, error contracts, pagination, idempotency and versioning: the choices that are cheap now and expensive to change later.

DA

Dimas Adit Thalia Putra

Fullstack Developer

REST API Design: Decisions That Age Well

An API is a public contract, even when the only consumer is your own frontend. Clients get built against it, and mistakes become permanent the moment they ship. These are the decisions I now make deliberately at the start.

Model resources, not actions

URLs should name things. HTTP methods supply the verb.

  • GET /invoices lists, POST /invoices creates
  • GET /invoices/42 reads, PATCH /invoices/42 updates, DELETE /invoices/42 removes
  • Use plural nouns and keep nesting shallow: /invoices/42/items is fine, four levels deep is not

When an operation really is not CRUD, such as approving a leave request, model it as a sub-resource or an action endpoint instead of overloading a field update: POST /leave-requests/17/approval.

Use status codes honestly

Clients branch on the status code, so it needs to mean something.

  • 200 and 201 for success, 204 when there is nothing to return
  • 400 for malformed input, 422 for valid syntax that fails validation
  • 401 when unauthenticated, 403 when authenticated but not allowed
  • 404 for a missing resource, 409 for a state conflict
  • 429 for rate limiting, with a Retry-After header

Returning 200 with { "success": false } forces every client to parse the body to learn what happened.

One consistent error shape

Pick a single error format and use it everywhere, including for unexpected failures.

{
  "error": {
    "code": "validation_failed",
    "message": "The request contains invalid fields.",
    "details": [
      { "field": "email", "message": "Must be a valid email address." }
    ],
    "requestId": "req_8f2c1a"
  }
}

A stable machine-readable code lets clients react programmatically. The requestId lets you find the matching log line when someone reports a problem.

Paginate from day one

Unbounded list endpoints work until the table grows. Add pagination before you need it, because adding it later is a breaking change.

  • Offset pagination (?page=2&per_page=25) is simple and fine for admin tables
  • Cursor pagination (?after=<token>) stays fast and stable on large or frequently changing data

Always enforce a maximum page size on the server.

Make unsafe operations idempotent

Networks retry. If a client sends POST /payments twice because a response was lost, you want one payment, not two. Accept an Idempotency-Key header, store the key with the result, and return the stored response on a repeat.

Validate at the boundary

Never trust input shape. Validate type, range and length at the edge, and keep the rules in one place. Whitelist the fields that may be set so a client cannot mass-assign role: "admin".

Plan for change

  • Prefer additive changes: new optional fields rarely break anyone
  • Never repurpose or change the meaning of an existing field
  • When you must break something, version it (/v2) and give consumers a deprecation window with a clear date
  • Document the contract with OpenAPI so the spec and the behaviour stay in sync

Do not forget the basics

  • Authenticate every request and authorize per resource, not per route
  • Rate limit by client, not only by IP
  • Log request IDs, status codes and latency
  • Return only the fields the client needs; never serialize a model wholesale

Good API design is mostly restraint. Boring, consistent conventions are what make an API pleasant to integrate and cheap to maintain.

  • #REST
  • #API Design
  • #Backend

Keep reading

Portfolio©2026

DIMAS

Loading the experience

000