RESTful API Cheatsheet - REST API Design Reference
APIs rarely degrade because features are missing; they degrade because shapes drift. The same resource gets a different name in different endpoints, the same class of failure returns HTTP 200 with an errCode in one place and HTTP 500 in another, and every frontend integration has to re-derive the rules. This sheet breaks each decision out separately: how URLs encode resource hierarchy and collections, why nouns plural beat verbs, whether filters, pagination and sorting belong in query parameters, how methods map onto idempotency, which status code fits which situation, which cross-cutting concerns belong in headers, what a fixed response and error envelope look like, and which auth scheme fits which risk. Sequence matters too - settle the resource model and URL first, because they determine authorisation and caching; lock the error envelope next, because that is what frontend fallbacks branch on; choose the success wrapper last, since changing it is cheapest. The division of labour with the method sheet is clean: that one picks a verb, this one keeps an entire surface consistent.
Typical Use Case
For back-end engineers designing, reviewing or implementing HTTP interfaces. The sheet settles the conventions in one pass: plural nouns for collections, IDs for single resources, query parameters for filtering, paging and sorting, and one line per method on safety and idempotency. Status codes come with their semantic edges, notably 401 versus 403 and 400 versus 422. Authentication covers Bearer tokens, API keys and the OAuth 2.0 authorization code flow, plus ETag conditional requests, data plus meta envelopes and batch sub-resources.
URL Design Spec 6
/api/v1/users/api/v1/users/123/api/v1/users/123/orders/api/v1/users?role=admin/api/v1/users?page=2&limit=20/api/v1/users?sort=-created_atHTTP Methods 8
GET /usersGET /users/123POST /usersPUT /users/123PATCH /users/123DELETE /users/123HEAD /users/123OPTIONS /usersCommon Status Codes 12
200 OK201 Created204 No Content301 Moved Permanently400 Bad Request401 Unauthorized403 Forbidden404 Not Found409 Conflict422 Unprocessable Entity429 Too Many Requests500 Internal Server ErrorRequest Headers 5
Content-Type: application/jsonAccept: application/jsonAuthorization: Bearer <token>If-None-Match: "etag"X-API-Key: abc123Response Format 5
{ "data": [...], "meta": {...} }{ "data": {...} }{ "error": { "code": "...", "message": "..." } }{ "page": 2, "limit": 20, "total": 100 }Link: <url>; rel="next"Auth Methods 5
Authorization: Basic base64(user:pass)Authorization: Bearer <jwt>X-API-Key: <key>?api_key=<key>OAuth 2.0Best Practices 5
Idempotency designPOST create returns 201 + LocationBulk operations: POST /users/batchRateLimit-Limit / Retry-AfterHATEOASParameter matrix
| Parameter | Effect | Example |
|---|---|---|
/api/v1/users | Resource collection path, with the noun in plural. | /api/v1/users |
/api/v1/users/123 | Locate a single resource by ID. | /api/v1/users/123 |
/api/v1/users/123/orders | Nested sub-resource path. | /api/v1/users/123/orders |
?role=admin | Filter resources with query parameters. | /api/v1/users?role=admin |
?page=2&limit=20 | Pagination parameters. | /api/v1/users?page=2&limit=20 |
?sort=-created_at | Sort parameter; the leading - means descending. | /api/v1/users?sort=-created_at |
GET /users | Fetch a resource list; safe and idempotent. | GET /users |
POST /users | Create a resource; not idempotent. | POST /users |
PUT /users/123 | Fully update a resource; idempotent. | PUT /users/123 |
PATCH /users/123 | Partially update a resource. | PATCH /users/123 |
DELETE /users/123 | Delete a resource; idempotent. | DELETE /users/123 |
HEAD /users/123 | Fetch only the response headers, no body. | HEAD /users/123 |
OPTIONS /users | Ask which methods are supported — this is the CORS preflight. | OPTIONS /users |
200 OK | The request succeeded. | GET /users/123 → 200 |
201 Created | The resource was created; pair it with a Location header. | POST /users → 201 + Location |
204 No Content | Success with an empty body, as after a delete. | DELETE /users/123 → 204 |
301 Moved Permanently | Permanent redirect for the resource. | GET /old → 301 |
400 Bad Request | The parameters are wrong or the payload is malformed. | POST /users → 400 |
401 Unauthorized | Not authenticated: the credential is missing or invalid. | no Authorization header -> 401 |
403 Forbidden | Authenticated, but not allowed to touch this resource. | regular user calls an admin endpoint -> 403 |
404 Not Found | No such resource. | GET /users/999 → 404 |
409 Conflict | The request collides with current state, such as a duplicate create. | duplicate signup for the same account name -> 409 |
422 Unprocessable Entity | The syntax was fine but field validation failed. | malformed email address -> 422 |
429 Too Many Requests | Rate limit exceeded; the caller has been throttled. | flood of calls from one client -> 429 |
500 Internal Server Error | The server failed internally. | unhandled exception -> 500 |
Content-Type: application/json | Declare that the request body is JSON. | Content-Type: application/json |
Accept: application/json | Ask for a JSON response. | Accept: application/json |
Authorization: Bearer *** | Bearer token or JWT authentication. | Authorization: Bearer <token> |
If-None-Match: "etag" | Conditional request that revalidates a cached copy with an ETag. | If-None-Match: "v1" |
X-API-Key: *** | API key authentication carried in a custom header. | X-API-Key: <key> |
{ "data": [...], "meta": {...} } | The conventional envelope for a list response. | { "data": [], "meta": {} } |
{ "data": {...} } | The conventional envelope for a single resource. | { "data": { "id": 1 } } |
{ "error": { "code", "message" } } | One error shape for every failure. | { "error": { "code": "INVALID_ARG" } } |
{ "page", "limit", "total" } | Pagination metadata. | { "page": 2, "limit": 20, "total": 100 } |
Link: <url>; rel="next" | Pagination links in a header, so clients can walk the pages. | Link: </users?page=3>; rel="next" |
Authorization: Basic *** | HTTP basic authentication. | Authorization: Basic <base64> |
?api_key=<key> | Pass the API key as a query parameter. | /users?api_key=<key> |
OAuth 2.0 authorization code flow | The standard flow for authorizing a third-party client. | authorization code flow |
POST /users/batch | Bulk create and update get their own sub-resource endpoint. | POST /users/batch |
RateLimit-Limit / Retry-After | Rate limit headers telling the caller its quota and when to retry. | Retry-After: 30 |
HATEOAS | Responses carry links to the related resources. | { "links": { "orders": "..." } } |
Common pitfalls
SymptomAfter receiving a 401 the client keeps retrying and never gets in.
CauseThe API also returns 401 for "invalid credentials", so the client cannot tell whether to re-authenticate or retry.
FixReturn 401 when unauthenticated and 403 when authenticated but unauthorized, so the client can take different branches.
SymptomThe front end treats a 400 as a field validation failure, but the request body format is actually wrong.
CauseFormat errors and semantic errors share the same status code.
FixUse 400 for format or syntax errors and 422 for field-level business validation failures, distinguishing them in error.code.
SymptomSubmitting the same create request twice produces two identical records in the database.
CausePOST is not idempotent by itself, so the server cannot recognize a duplicate when the client retries on timeout.
FixUse 409 Conflict for duplicate creation and require the client to send an idempotency key or query before creating.
SymptomAfter a PUT update, fields the client did not send are cleared.
CausePUT is treated as a full replacement, writing back missing fields as defaults.
FixWhen only part of the fields needs changing, switch to PATCH and accept only the fields actually submitted.
SymptomAfter switching to PATCH the client still submits a full object and the server behaves like PUT.
CauseThe API implementation ignores partial-update semantics and overwrites the whole record.
FixIn the PATCH handler, update only the keys present in the request body and leave the rest unchanged.
SymptomThe collection path is written /user instead of /users, and clients parsing by one rule get everything wrong.
CauseURL naming is inconsistent, so resource collections and single resources are not distinguished.
FixUse plural nouns for collections and IDs to address single resources, and pin the version in a /v1/ prefix.
SymptomAfter hitting the rate limit the client does not back off and the API is crushed in an avalanche.
CauseThe 429 response does not tell the client how long to wait, so it can only retry densely.
FixInclude Retry-After and RateLimit-Limit in the 429 response so the client backs off as instructed.
Troubleshooting
1Confirm which HTTP methods an endpoint actually supports.
OPTIONS /usersA preflight request returns the allowed methods list, also the first entry point for diagnosing CORS and method restrictions.
2Check whether the client's cached copy is still valid.
If-None-Match: "etag"Send the last ETag with the request; when the content is unchanged the server returns 304, saving the entire response body.
3Pin down which page pagination starts from and the total count.
/api/v1/users?page=2&limit=20The { "page", "limit", "total" } in the response lets you verify that the server and client agree on page numbering.
4Verify the default list ordering against creation time, descending.
/api/v1/users?sort=-created_atThe leading - means descending; removing it means ascending, so you can confirm the sort direction matches the API contract.
5Confirm the new resource address after a successful create.
POST /api/v1/usersSuccess should return 201, and the Location header gives the URI of the new resource, which you can use to follow up with a GET.
6Tell an authentication failure apart from an authorization failure
GET /api/v1/users/123No credential at all yields 401, while a valid credential that is still refused yields 403 — and that is how you know whether to fix the client token or the server policy.
7Confirm what a bulk endpoint actually does
POST /users/batchBulk work belongs on its own sub-resource rather than as extra verbs piled onto the collection, and the response should report success and failure item by item.
Command Examples
A list endpoint: filter, pagination and sort in one request
GET /api/v1/users?role=admin&page=2&limit=20&sort=-created_at
Accept: application/json
Authorization: Bearer <token>Plural nouns for collections, a sub-path per ID, and filters, pagination and sorting as query parameters (a leading - on sort means descending). Wrapping the payload in data with pagination in meta keeps one response shape for every client.
Output
HTTP/1.1 200 OK
Content-Type: application/json
{
"data": [ { "id": 123, "role": "admin", "created_at": "2026-07-19T10:30:00Z" } ],
"meta": { "page": 2, "limit": 20, "total": 100 }
}Creation returns 201 plus a Location header
POST /api/v1/users
Content-Type: application/json
{ "name": "tom" }A successful POST must return 201 with a Location header so clients never have to parse the body to build the new URL. Returning 200 makes creation indistinguishable from a read in both caches and monitoring.
Output
HTTP/1.1 201 Created
Location: /api/v1/users/456
{ "data": { "id": 456, "name": "tom" } }Cut whole-payload transfers with an ETag conditional request
GET /api/v1/users/123
If-None-Match: "a1b2c3d4"The client echoes the ETag it last stored; when nothing changed the server replies 304 with no body. For polling-heavy mobile endpoints this drops traffic to zero and skips server-side serialization entirely.
Output
HTTP/1.1 304 Not Modified ETag: "a1b2c3d4"
A fixed error envelope, with 401 and 403 kept distinct
GET /api/v1/users/999
Authorization: Bearer <expired-token>401 means the credential is missing or expired and the client should re-authenticate; 403 means the credential is valid but under-scoped, and retrying is pointless. Collapsing both into 401 sends clients into an endless refresh loop.
Output
HTTP/1.1 403 Forbidden
{
"error": {
"code": "FORBIDDEN",
"message": "token lacks scope users:read"
}
}Back off according to Retry-After after a rate limit
GET /api/v1/users
# when the rate limiter trips:
# HTTP/1.1 429 Too Many Requests
# RateLimit-Limit: 100
# Retry-After: 30A 429 without Retry-After forces clients to guess, which piles more load onto an already saturated service. Clients must treat 429 as a retryable state driven by the header, not as a terminal failure.
Common Pitfalls
- Cursor pagination depends on a stable sort: new inserts or ties invalidate the next cursor, so clients stall on one page or silently drop rows. Prefer a composite (created_at, id) cursor with a consistent snapshot.
- A POST without an Idempotency-Key or unique constraint turns a client retry after a timeout into a duplicate order or a second charge. Payment writes must be retry-safe; one network hiccup otherwise costs real money.
- Local-time timestamps lose timezone and precision: clients in other zones read 2026-07-19T10:30:00Z as local time and daily reports stop reconciling. Store UTC RFC 3339 with Z or an offset, and keep money as integer minor units.
- Passing the key as ?api_key= records it in browser history, proxy logs and the Referer header, which is effectively publishing the credential. Use the X-API-Key or Authorization header instead.
- PUT is an idempotent full replace, so omitting fields wipes them. Clients wanting partial updates must use PATCH, or fields vanish without any error.
- DELETE is idempotent, but delete-then-recreate makes the second DELETE return 404, which clients read as a failure. Return 204 or a receipt field when the client must confirm the outcome.
Tips
- Use plural nouns for resource collections in URLs, e.g. /users not /user.
- GET should be safe (no server state change); PUT/DELETE should be idempotent.
- PUT fully replaces; PATCH partially updates — choose by scenario.
- Put the API version in the URL path (/v1/) or a request header (Accept-Version).
- Error responses should include a code, message, and optional details for easy client handling.
FAQ
How do I choose between 401, 403 and 404?
Split them by category: authentication, authorisation, or absence. 401 Unauthorized means authentication failed — no credentials, an invalid token, or an expired one; the client fix is to attach or refresh the credential. 403 Forbidden means authentication succeeded but the principal lacks permission, so retrying is pointless. 404 Not Found means the resource does not exist, and is also the deliberate choice when you want to hide existence rather than reveal "it exists but you may not have it". Their retry semantics differ completely: 401 is retryable with a new token, 403 is not, 404 means fix the URL or the id. A duplicate create is none of these — that is 409 Conflict.
Both PUT and PATCH change data — how should an API design choose between them?
It is about replacement semantics. PUT /users/123 is a full update: the complete representation must be supplied, and calling it twice leaves the same result (idempotent). PATCH /users/123 is a partial update: send only the fields you want changed and the rest stay as they are. DELETE /users/123 is likewise idempotent — repeating it changes nothing. GET must be safe and never alters server state, while POST /users creates and is non-idempotent. In practice PATCH is the easier client contract since only changed fields travel; PUT is friendlier to validation and caching but silently drops data if a field is omitted, which is why many teams define a full-required schema plus a partial-optional one.
For an invalid request, do I return 400 or 422?
Layer them by where the failure occurs. 400 Bad Request is for a malformed request: the JSON will not parse, the Content-Type is wrong, the syntax itself is broken and the server never got as far as reading the content. 422 Unprocessable Entity is for semantic failure — the structure parses but field validation fails: a bad email format, an out-of-range age, an illegal status value. The split matters to clients, because 400 means "do not resend this unchanged" while 422 means "fix the flagged fields and resend". Both should use the same envelope, { "error": { "code": "...", "message": "..." } }, so clients can branch on the code.
What should a create endpoint return, and how are batch writes and rate limiting handled?
A successful create should return 201 Created rather than 200, with a Location header pointing at the new resource URI; fetch it afterwards with HEAD /users/123 (headers only) or GET for the body. When there is nothing to return, use 204 No Content — the classic case being delete. Do not force batch creates or updates through an ordinary POST; use a sub-resource endpoint such as POST /users/batch. When throttling kicks in, return 429 Too Many Requests together with RateLimit-Limit for the quota and Retry-After for when to come back, so the client can back off. Including links to related resources (HATEOAS) lets clients discover the next action instead of hard-coding URLs.
How do I choose between the authentication options listed here?
Pick by client type and trust level. Authorization: Bearer <token> suits a separated front end and SPA; the token may be a JWT so the permissions travel with it. Authorization: Basic <base64> is only base64-encoded username and password — weak, fine for an intranet or quick debugging. X-API-Key: <key> uses a custom header and is the most common server-to-server credential; putting the key in ?api_key=<key> is convenient but leaks into access logs and Referer headers, so keep it out of production. Use OAuth 2.0 in the authorization code flow when third-party users must grant access. Pair these with Content-Type: application/json, Accept: application/json and If-None-Match: "etag" so an ETag hit serves from cache and saves bandwidth.
Official References
Each command links to its official documentation below, so you can verify the latest usage and read deeper.
Maintained by LaoHand
Publicly updated on Jul 21, 2026, continuously proofread against official docs.
Contact Us
Wrong command or description? Send us corrections, business inquiries or product feedback by email.
Contact Us