WierdX — Programming Reference All tutorials →
Developer reference · Practical tutorials · CS fundamentals
API Design

REST API Design Principles Every Developer Should Know

A well-designed REST API is predictable, evolvable, and easy to consume without reading the source. The design decisions you make on day one — how you name resources, which status codes you return, how you handle errors — are the ones you will be living with for years.

Published June 25, 2026

REST (Representational State Transfer) is an architectural style described by Roy Fielding in his 2000 doctoral dissertation. It is not a protocol or a standard: it is a set of constraints that, when followed, produce APIs with useful properties — statelessness, cacheability, a uniform interface. In practice, "REST API" means an HTTP API that uses URLs to identify resources and HTTP methods to express operations on them.

Most teams follow enough of the constraints to get the benefits without needing to study the dissertation. What follows are the specific design decisions that determine whether your API is a pleasure or a burden to work with.

Name resources, not actions

The most common early mistake is designing URLs around verbs rather than nouns. REST URLs should identify things, and HTTP methods should express what you want to do to them.

# Verb-oriented (not REST)
GET  /getUser?id=42
POST /createOrder
POST /deleteProduct?id=7

# Resource-oriented (REST)
GET    /users/42
POST   /orders
DELETE /products/7

Use plural nouns for collection endpoints (/users, /orders). Use the resource identifier in the path for individual items (/users/42). Nest sub-resources only when the relationship is ownership and the child cannot reasonably exist without the parent: /orders/19/items is fine; /users/42/all-related-products is a smell indicating you need a query parameter or a separate endpoint instead.

Keep hierarchy shallow. More than two or three levels of nesting (/orgs/5/teams/3/members/12/roles) makes URLs unwieldy and couples the client to your data model. Flatter is almost always better: /members/12/roles.

Use HTTP methods for what they mean

HTTP defines a small vocabulary of methods, each with a precise semantic meaning that clients and infrastructure (proxies, caches, load balancers) understand:

  • GET: retrieve a resource or collection. Safe (no side effects) and idempotent (multiple identical requests produce the same result). Responses may be cached.
  • POST: create a new resource or trigger an operation. Not idempotent — submitting the same request twice may create two records.
  • PUT: replace a resource entirely with the supplied representation. Idempotent — submitting the same PUT twice produces the same state.
  • PATCH: apply a partial update to a resource. Not inherently idempotent, though implementations often are.
  • DELETE: remove a resource. Idempotent — deleting an already-deleted resource should return 200 or 204, not 404 on the second call.

The practical consequence of GET being safe is that browsers and proxies feel free to retry it, prefetch it, and cache it. If your GET endpoint triggers a state change, you will see phantom writes when a proxy retries a slow request or a browser prefetches a link.

Return the right status codes

Status codes are part of your API's contract. Clients use them to decide what to do next without parsing the body. Using 200 OK for everything, even errors, forces clients to inspect every response body — breaking retry logic, monitoring, and any generic HTTP client behavior.

201 Created       -- new resource created (include Location header)
204 No Content    -- success with no body (DELETE, some PUTs)
400 Bad Request   -- client sent invalid data
401 Unauthorized  -- not authenticated (misleading name in the spec)
403 Forbidden     -- authenticated but not authorized
404 Not Found     -- resource does not exist
409 Conflict      -- state conflict (duplicate create, stale update)
422 Unprocessable Entity -- request is well-formed but semantically invalid
429 Too Many Requests    -- rate limit hit (include Retry-After header)
500 Internal Server Error -- server fault, client should retry with backoff

Error responses should include a structured body that tells the client what went wrong and, where possible, what to do about it:

{
  "error": "validation_failed",
  "message": "The email field must be a valid email address.",
  "field": "email"
}

A machine-readable error code (not the HTTP status) lets clients handle specific errors programmatically. A human-readable message helps developers debugging. Field-level context makes form validation straightforward on the client side.

Version your API from the start

APIs change. Consumers do not always update immediately. Without versioning, any breaking change forces all consumers to update simultaneously — which is rarely possible. Versioning gives you room to evolve the API without breaking existing clients.

The two most common strategies are URL path versioning and header versioning:

# URL versioning -- simple, explicit, cache-friendly
GET /v1/users/42
GET /v2/users/42

# Accept-header versioning -- cleaner URLs, harder to test in a browser
GET /users/42
Accept: application/vnd.myapi.v2+json

URL versioning is more common in public APIs because it is visible in every request, easy to route in a load balancer, and trivial to test with curl. Header versioning is preferred by purists because REST considers the URL to identify the resource, not the representation version — but the practical tooling advantages of URL versioning usually win.

Version the API at the point where the contract becomes stable, not before. Internals-only APIs between services you control can often skip formal versioning and rely on coordinated deploys instead.

Paginate large collections

Returning an entire collection in one response does not scale. A GET /orders that returns 500,000 rows will time out, exhaust memory on the server, and make the client unresponsive. Pagination is required for any collection that grows over time.

The two main approaches are offset pagination and cursor pagination:

# Offset pagination: simple but fragile under concurrent writes
GET /orders?limit=50&offset=100

# Cursor pagination: stable, efficient, safe under writes
GET /orders?limit=50&after=cursor_eyJpZCI6MTAwfQ

Offset pagination is easy to implement and supports random access (jump to page 7), but it produces skipped or duplicated rows when records are inserted or deleted between requests. Cursor pagination encodes a position in the data (usually a stable sort key like an ID or timestamp), is immune to concurrent writes, and uses an index-seek rather than OFFSET — making it far faster on large tables.

Return pagination metadata in a consistent envelope or in response headers:

{
  "data": [...],
  "pagination": {
    "next_cursor": "cursor_eyJpZCI6MTUwfQ",
    "has_more": true
  }
}

Design for cacheability

HTTP has a sophisticated caching model built in. Exploiting it reduces load on your servers and improves latency for clients. The two main tools are Cache-Control headers and conditional requests.

# Tell clients and proxies how long to cache the response
Cache-Control: public, max-age=300

# Revalidation: client sends ETag back; server returns 304 if unchanged
ETag: "abc123"
If-None-Match: "abc123"  -- client sends this; server responds 304 or 200

Resources that rarely change (reference data, configuration, public content) benefit from longer max-age values. Resources that change per-user or per-request should be marked Cache-Control: private or no-store. Conditional requests with ETag or Last-Modified allow clients to revalidate cheaply: a 304 Not Modified sends just the headers, saving the body transfer entirely.

Keep state on the server, not in the URL

REST's statelessness constraint means each request must contain all the information the server needs to fulfill it. The server does not remember anything from previous requests. Authentication tokens, user IDs, and any context needed to process the request belong in the request itself — in headers or the body — not stored in server-side session memory indexed by a session cookie.

Stateless APIs scale horizontally without sticky sessions. Any server instance can handle any request. Session-based APIs require either sticky routing (sending the same client to the same server) or session replication (sharing session state across all servers), both of which add operational complexity.

The practical implication for API design: use JWT or similar self-contained tokens rather than opaque session identifiers. The token carries the claims the server needs to authorize the request, so no database lookup is required to validate identity.

Document the contract, not the implementation

An API without documentation is a private API regardless of how many clients use it. Good documentation describes the contract — the URLs, methods, request shapes, response shapes, and error codes — not how the implementation works internally. OpenAPI (formerly Swagger) is the de facto standard for HTTP API documentation; generating it from code annotations keeps the documentation and the implementation in sync.

Treat breaking changes with the same seriousness as database migrations: they require versioning, a deprecation period, and coordinated communication with consumers. Non-breaking changes — adding new optional fields, adding new endpoints, adding new status codes — can be deployed without incrementing the version, provided you have tested that existing clients ignore unknown fields.