~/blog/-blog-rest-api-design-guide-
blog · HTTP

REST API Design: The Conventions That Actually Matter in Production

URL structure, HTTP method semantics, versioning, error response shapes, and pagination — the REST API conventions that prevent client breakage and debugging nightmares.

last updated · June 14, 2026by @vultio

REST is a style, not a standard — and that matters

REST (Representational State Transfer) is an architectural style defined by Roy Fielding in his 2000 dissertation. It describes constraints — statelessness, uniform interface, resource identification — but it does not prescribe exact conventions for URLs, error formats, or versioning. This freedom is why "RESTful API" can describe both a beautifully consistent API and an incoherent collection of endpoints held together by HTTP.

The conventions that follow are not REST dogma — they are practices that have proven to reduce integration friction in real production APIs. Their value is not philosophical correctness; it is that clients that integrate with your API can make reasonable predictions about how it behaves, without reading every endpoint's documentation individually.

URL structure: resources, not actions

URLs should identify resources (nouns), not operations (verbs). The HTTP method expresses the operation. This separation is what makes REST URLs predictable.

# Bad — verbs in URLs, RPC style
POST /createUser
GET  /getUser?id=123
POST /updateUser
POST /deleteUser?id=123
POST /getUserOrders?userId=123

# Good — nouns in URLs, HTTP method expresses the action
POST   /users              # create a user
GET    /users/123          # get user 123
PUT    /users/123          # replace user 123
PATCH  /users/123          # partial update user 123
DELETE /users/123          # delete user 123
GET    /users/123/orders   # get orders belonging to user 123

Use lowercase, hyphen-separated path segments (/payment-methods, not/paymentMethods or /payment_methods). Use plural nouns for collections (/users, /orders). Keep nesting shallow — more than two levels deep (/users/123/orders/456/items) suggests the resource may need its own top-level endpoint.

HTTP method semantics

GET    — read, safe (no side effects), idempotent, cacheable
POST   — create or submit, not idempotent (multiple calls create multiple resources)
PUT    — replace entire resource, idempotent (same result on repeat)
PATCH  — partial update, may or may not be idempotent depending on implementation
DELETE — delete resource, idempotent (deleting twice has the same result)
HEAD   — same as GET but response body omitted (check if resource exists/etag)
OPTIONS — describe supported methods (used in CORS preflight)

Idempotent means: calling the same request N times has the same effect as calling
it once. GET, PUT, DELETE are idempotent. POST is not. This has practical implications:
— Idempotent requests are safe to retry automatically on network failure
— POST requests should only be retried explicitly (duplicate prevention required)

The most commonly misused method is POST vs PUT. POST creates a new resource with a server-assigned ID. PUT creates or replaces a resource at a client-specified URL. If the client decides the resource's identifier, use PUT. If the server assigns the ID, use POST. PATCH is appropriate for partial updates where you send only the fields being changed — the client does not need to know the full current state of the resource.

HTTP status codes: the right code for every outcome

# Success
200 OK            — GET, PUT, PATCH (returned with response body)
201 Created       — POST (new resource created; include Location header with new URL)
204 No Content    — DELETE, PUT with no response body
202 Accepted      — async operation started (not yet complete)

# Client errors (4xx — the client did something wrong)
400 Bad Request   — invalid JSON, missing required field, validation error
401 Unauthorized  — missing or invalid authentication credentials
403 Forbidden     — authenticated but not authorised for this resource
404 Not Found     — resource does not exist
409 Conflict      — state conflict (duplicate unique value, wrong version)
422 Unprocessable — valid JSON, but semantic validation failed
429 Too Many Requests — rate limit exceeded (include Retry-After)

# Server errors (5xx — the server did something wrong)
500 Internal Server Error — unexpected error (should be logged and alerted)
502 Bad Gateway   — upstream service returned an error
503 Service Unavailable — overloaded or down for maintenance
504 Gateway Timeout — upstream service timed out

Consistent error response shape

The single most important consistency decision for an API is the error response format. Clients need to parse errors programmatically. If every endpoint returns errors in a different shape, clients must handle each one specially. Pick a format and use it everywhere, including validation errors, authentication failures, and unexpected server errors.

// A practical error format
{
  "error": {
    "code": "VALIDATION_ERROR",      // machine-readable code, stable across versions
    "message": "Validation failed",  // human-readable, may change
    "details": [                     // optional: field-level errors
      {
        "field": "email",
        "message": "Invalid email address"
      },
      {
        "field": "password",
        "message": "Must be at least 8 characters"
      }
    ],
    "request_id": "req_abc123"       // correlates to server logs for debugging
  }
}

// 404 error
{
  "error": {
    "code": "NOT_FOUND",
    "message": "User with id 'usr_999' not found",
    "request_id": "req_xyz789"
  }
}

The code field is the most important: it is a string constant that clients can switch on without parsing the human-readable message. The message can change as you improve your error copy; the code should be versioned and stable. The request_id is invaluable for support — when a user reports an error, they provide the request ID, and you can find the exact server-side logs in seconds.

Versioning

Version your API from the start, even if you only have one version. Adding versioning after clients are in production is significantly harder than including it from day one. The two most common approaches are URL path versioning and header versioning.

# URL path versioning (most common, easiest to work with)
https://api.example.com/v1/users
https://api.example.com/v2/users   # v2 can have a different response shape

# Header versioning (cleaner URLs, harder to test in browser)
GET /users HTTP/1.1
Host: api.example.com
API-Version: 2026-06-14   # date-based (Stripe's approach)
# or
Accept: application/vnd.example.v2+json  # content negotiation

URL path versioning is the pragmatic choice for most APIs — it is visible in logs, bookmarkable, testable without special tools, and works with every HTTP client without configuration. Use it unless you have a specific reason not to.

Pagination

Never return unbounded collections. An endpoint that returns all users may work in development with 100 test records and time out in production with 10 million. Paginate every collection endpoint from the start.

// Cursor-based pagination (preferred for large, frequently-updated datasets)
// Request
GET /orders?limit=50&after=ord_abc123

// Response
{
  "data": [...],
  "pagination": {
    "has_more": true,
    "next_cursor": "ord_xyz789",    // opaque token, not a page number
    "total": null                   // often omitted — COUNT(*) is expensive
  }
}

// Offset-based pagination (simpler, but inconsistent on live data)
// Request
GET /orders?limit=50&offset=100

// Response
{
  "data": [...],
  "pagination": {
    "total": 1247,
    "limit": 50,
    "offset": 100,
    "has_more": true
  }
}

Cursor-based pagination is more reliable for real-time data — offset pagination can return duplicates or skip rows if records are inserted or deleted between page requests. For most applications, cursor-based pagination with an opaque cursor token is the better default. If you need the user to jump to a specific page number (like a search results UI), offset pagination is acceptable despite its consistency limitations.