REST API design best practices: resource naming, HTTP methods, and status codes
How to design a clean REST API: plural resource URLs, safe and idempotent HTTP methods, meaningful status codes, pagination, and error responses — with practical examples.
On this page
- Design resources around nouns, not verbs
- Use the HTTP methods correctly
- Use status codes that mean something
- Paginate large collections
- Return consistent, structured errors
- Add API keys and versioning early
- FAQ
- What makes a URL a good REST resource URL?
- Should I use PUT or PATCH to update a resource?
- How do I choose the right status code?
- What is idempotency and why does it matter for retries?
A well-designed REST API is easy to read, easy to use, and safe to retry. This guide covers the practices that matter most in practice: resource naming, choosing the right HTTP methods, returning meaningful status codes, paginating large collections, and structuring errors so clients can handle them automatically.
Design resources around nouns, not verbs
REST is a software architectural style that treats data and functionality as resources identified by URLs. The core principle is a uniform interface: resources are identified in requests by URIs, manipulated through representations (usually JSON), and described by self-contained messages.
In practice, this means two rules:
- Use plural nouns for collections. A list of books lives at
/books; a single book at/books/42. The collection is plural, the item is reached by appending its identifier. - Let the HTTP method express the action — never put CRUD verbs in the URL.
/bookswithPOSTcreates,/books/42withDELETEdeletes. Avoid URLs like/createBookor/deleteBookById: the method already says it, and verb-in-URL URLs break the uniform interface.
Nesting is fine for sub-resources: /books/42/reviews returns the reviews of book 42. But avoid going deep — two levels is usually enough, and relationships beyond that are often better reached by query parameters.
Use the HTTP methods correctly
HTTP defines methods with precise semantics. Choosing the right one makes your API predictable and lets clients rely on safe and idempotent behavior.
| Method | What it does | Safe | Idempotent |
|---|---|---|---|
| GET | Retrieve a representation of the resource | Yes | Yes |
| HEAD | Same as GET but no response body | Yes | Yes |
| POST | Submit an entity, often creating a resource or causing side effects | No | No |
| PUT | Replace the entire target resource | No | Yes |
| PATCH | Apply a partial modification | No | No |
| DELETE | Delete the target resource | No | Yes |
Safe means the request does not change server state. Idempotent means repeating the request produces the same result. GET, PUT, and DELETE are idempotent, so a client that loses the response to a network error can safely retry them. POST and PATCH are not, so a client should not blindly retry them — that is why creating a resource with POST /books is followed by a 201 Created, and an update uses PUT (full replace) or PATCH (partial change) so retries stay harmless.
Use status codes that mean something
HTTP response status codes fall into five classes, and each code tells the client something specific:
- 2xx success:
200 OKfor a normal successful read or write,201 Createdwhen a resource was created,204 No Contentwhen an operation succeeded but there is nothing to return (for example, a successfulDELETE). - 3xx redirection: the resource moved; clients should follow the
Locationheader. - 4xx client error: the request was understood but cannot be fulfilled.
400 Bad Requestfor malformed input,401 Unauthorizedwhen authentication is missing or invalid,403 Forbiddenwhen the client is authenticated but not allowed,404 Not Foundwhen the resource does not exist,409 Conflictwhen the request clashes with the current state,422 Unprocessable Entityfor well-formed requests that fail validation, and429 Too Many Requestswhen rate-limited. - 5xx server error:
500 Internal Server Errorfor unexpected failures and503 Service Unavailablewhen the server is overloaded or down for maintenance.
Using the right code is more than cosmetics: automated clients branch on these codes, and caches and proxies behave differently based on them.
Paginate large collections
A GET on a large collection should not return every row. The two most common approaches are offset-based and cursor-based pagination.
GET /books?limit=20&offset=0
Offset pagination (offset and limit) is simple and lets clients jump to any page, but it gets slow and drifting on large, frequently-changing datasets because rows shift as they are added or removed. Cursor-based pagination uses an opaque token to keep the position stable:
GET /books?limit=20&cursor=abc123
The response includes a next_cursor (and possibly prev_cursor) field that the client passes back to fetch the next page. Regardless of approach, include the total or pagination metadata in the response so clients can render paging controls.
Return consistent, structured errors
Clients need enough information to react without parsing human prose. A helpful error response carries the status code plus a consistent JSON shape:
{
"error": {
"code": "validation_failed",
"message": "title is required",
"details": { "field": "title", "reason": "missing" }
}
}
Keep the structure the same on every endpoint, use a stable machine-readable code (not just a translated message), and reserve message for a developer-facing explanation. This makes retries, logging, and client-side handling uniform.
Add API keys and versioning early
Public APIs authenticate clients with an API key — a secret string sent in a request header. Treat keys like passwords: never store them in client-side code or public repositories. The server uses the key to identify you and apply rate limits; a missing or invalid key returns 401 Unauthorized, and exceeding a limit returns 429 Too Many Requests.
Version your API from the start, because changing an endpoint later breaks every client. A common pattern is a path prefix such as /v1/books, which lets old and new versions live side by side while clients migrate.
FAQ
What makes a URL a good REST resource URL?
Use plural nouns for collections (/books), append the identifier for a single item (/books/42), keep names lowercase with hyphens between words, and let HTTP methods express the action instead of verbs inside the URL. Keep nesting shallow — one or two levels.
Should I use PUT or PATCH to update a resource?
Use PUT to replace the entire resource: it is idempotent, so a retry is safe. Use PATCH for a partial update of only the fields you pass. Because PATCH is not idempotent, be careful about retrying it after a network failure.
How do I choose the right status code?
Pick from the class that matches the outcome: 2xx for success (create → 201, no content → 204), 4xx for client errors (bad input → 400, unauthenticated → 401, forbidden → 403, missing → 404, conflict → 409, validation → 422, rate-limited → 429), and 5xx for server failures (500 unexpected, 503 overloaded/maintenance).
What is idempotency and why does it matter for retries?
An idempotent request produces the same result no matter how many times it is repeated. GET, PUT, and DELETE are idempotent, so a client can safely retry them after a network error without causing duplicate side effects. POST and PATCH are not, so clients should not blindly retry them.
Sources
- Wikipedia — Representational state transfer (en.wikipedia.org/wiki/Representational_state_transfer
- CC BY-SA 4.0)
- MDN Web Docs — HTTP overview (developer.mozilla.org/en-US/docs/Web/HTTP/Overview
- CC-BY-SA 2.5+)
- MDN Web Docs — HTTP request methods (developer.mozilla.org/en-US/docs/Web/HTTP/Methods
- CC-BY-SA 2.5+)
- MDN Web Docs — HTTP response status codes (developer.mozilla.org/en-US/docs/Web/HTTP/Status
- CC-BY-SA 2.5+)
- IETF RFC 9110 — HTTP Semantics (rfc-editor.org/rfc/rfc9110)
- IETF RFC 6585 — Additional HTTP Status Codes (rfc-editor.org/rfc/rfc6585)
Share this guide
Related guides
- What is an API? A plain-language guide to how APIs work
What an API is, how REST APIs work over HTTP, the methods you'll actually meet — GET, POST, PUT, PATCH, DELETE — and how to read a response.
- What is WebAssembly (WASM)? How it works and when to use it
WebAssembly is a binary instruction format for a stack-based virtual machine — a portable compilation target that lets you run C, C++, Rust, and other compiled languages in the browser and beyond. Here's how it works and when it's worth using.
- What is the JavaScript event loop? How the call stack, task queue, and microtask queue work
The JavaScript event loop is the runtime model that lets single-threaded JavaScript run asynchronous code. Learn how the call stack, the task queue, and the microtask queue cooperate, and why Promise callbacks fire before setTimeout.