REST API Design Cheat Sheet
Reference for designing clean REST APIs: resource naming, HTTP methods, status codes, pagination, versioning, and best practices.
Resource Naming Conventions
How to structure clean, predictable URLs.
- Nouns, not verbs- Use /users not /getUsers — the HTTP method conveys the action
- Plural resource names- /orders not /order for collections
- Nesting- /users/42/orders for a sub-resource scoped to a parent
- Filtering via query params- /orders?status=shipped&sort=-createdAt
- Lowercase, hyphenated- /order-items not /orderItems or /OrderItems
- Versioning- /v1/users in the URL, or an Accept header-based scheme
HTTP Methods & Example
Standard CRUD verbs and a request/response example.
GET /users # List users (safe, idempotent)GET /users/42 # Retrieve a single userPOST /users # Create a new userPUT /users/42 # Replace user 42 entirely (idempotent)PATCH /users/42 # Partially update user 42DELETE /users/42 # Remove user 42 (idempotent)# Example request/responsePOST /users HTTP/1.1Content-Type: application/json{"name": "Ada Lovelace", "email": "[email protected]"}HTTP/1.1 201 CreatedLocation: /users/42Content-Type: application/json{"id": 42, "name": "Ada Lovelace", "email": "[email protected]"}
Pagination & Versioning
Offset vs cursor pagination, and two common versioning strategies.
# Offset-based paginationGET /orders?limit=20&offset=40# Cursor-based pagination (preferred for large/changing datasets)GET /orders?limit=20&cursor=eyJpZCI6MTAwfQ==# Response includes pagination metadata{ "data": [ ... ], "meta": { "nextCursor": "eyJpZCI6MTIwfQ==", "hasMore": true }}# Versioning strategiesGET /v1/orders # URI versioningGET /orders Accept: application/vnd.myapi.v2+json # header versioning
Best Practices
Habits that keep a REST API consistent and maintainable.
- Statelessness- Each request must carry all context needed; no server-side session state
- Proper status codes- 201 for creation, 204 for empty success, distinct 400 vs 404
- Consistent error format- e.g. { "error": { "code": "...", "message": "..." } } on every endpoint
- Idempotency keys- For POST operations that must be safely retried (e.g. payments)
- Rate-limit headers- X-RateLimit-Limit / X-RateLimit-Remaining / Retry-After
- HATEOAS (optional)- Include hypermedia links so clients can discover related actions
RFC 7807 Problem Details for Errors
Standardized machine-readable error payload instead of ad-hoc error shapes.
POST /orders HTTP/1.1Content-Type: application/json{"items": []}HTTP/1.1 422 Unprocessable EntityContent-Type: application/problem+json{ "type": "https://api.example.com/errors/empty-order", "title": "Order must contain at least one item", "status": 422, "detail": "The 'items' array was empty for order creation.", "instance": "/orders", "errors": [ { "field": "items", "code": "min_length", "message": "must contain >= 1 item" } ]}// type/title/status/detail/instance are the RFC 7807 fields;// extension members (like "errors") add API-specific detail
Conditional Requests & Optimistic Concurrency
Using ETag / If-Match to avoid lost updates on concurrent PUT/PATCH.
# 1. Client fetches the resource and receives an ETagGET /documents/42 HTTP/1.1HTTP/1.1 200 OKETag: "a1b2c3-v7"{"id": 42, "title": "Draft", "body": "..."}# 2. Client sends the update with If-Match so it fails if the resource changed underneath itPATCH /documents/42 HTTP/1.1If-Match: "a1b2c3-v7"Content-Type: application/json{"title": "Final"}# 3a. No conflict -> 200 with a new ETagHTTP/1.1 200 OKETag: "a1b2c3-v8"# 3b. Someone else updated it first -> reject instead of silently overwritingHTTP/1.1 412 Precondition Failed# GET responses can also short-circuit with If-None-Match to save bandwidthGET /documents/42 HTTP/1.1If-None-Match: "a1b2c3-v8"HTTP/1.1 304 Not Modified
Sparse Fieldsets & Resource Embedding
Letting clients shape the response to cut over-fetching and N+1 follow-up calls.
# Sparse fieldset: only return the fields the client needsGET /users/42?fields=id,name,email{"id": 42, "name": "Ada Lovelace", "email": "[email protected]"}# Embedding related resources to avoid a second round tripGET /orders/900?embed=customer,items.product{ "id": 900, "customer": { "id": 42, "name": "Ada Lovelace" }, "items": [ { "sku": "X-1", "qty": 2, "product": { "id": "X-1", "name": "Widget" } } ]}# Rule of thumb: default responses stay lean; fields/embed are opt-in,# never opt-out, so unaware clients keep working after new fields are added
Rate Limiting: Token Bucket Response Contract
Standard headers and 429 handling for a token-bucket limiter.
GET /search?q=api HTTP/1.1HTTP/1.1 200 OKX-RateLimit-Limit: 100X-RateLimit-Remaining: 37X-RateLimit-Reset: 1739980800# Once the bucket is emptyHTTP/1.1 429 Too Many RequestsRetry-After: 42Content-Type: application/problem+json{ "type": "https://api.example.com/errors/rate-limited", "title": "Rate limit exceeded", "status": 429, "detail": "Limit of 100 requests/min exceeded; retry after 42 seconds"}# Server-side sketch (pseudocode): refill tokens continuously, not in a fixed# window, to avoid the thundering-herd retry spike a hard reset window causes# tokens = min(capacity, tokens + elapsed_seconds * refill_rate)
REST vs GraphQL vs gRPC — When to Reach for Each
Architectural trade-offs beyond basic REST, for choosing an API style.
- REST + HTTP caching- Best when resources map cleanly to URLs and CDN/browser caching matters
- GraphQL- Best when clients have very different data shapes and over/under-fetching is a real cost
- gRPC- Best for internal service-to-service calls needing low latency and strict typed contracts
- Richardson Maturity Model- Level 0 (RPC over HTTP) -> 1 (resources) -> 2 (verbs+status codes) -> 3 (HATEOAS)
- Bulk/batch endpoints- POST /orders/batch to avoid hundreds of round trips REST purism would otherwise require
- Webhooks as the async counterpart- Long-running work returns 202 Accepted + a callback/webhook instead of blocking the request
Design around cacheability from day one — mark idempotent GET responses with proper Cache-Control and ETag headers so clients and CDNs can skip redundant round trips instead of hitting your API every time.