Designing a REST API – Principles, Patterns, and Common Mistakes
REST has been the dominant API architectural style for over two decades. It is not the newest or most fashionable approach — GraphQL and gRPC compete for attention — but REST remains the most widely understood and implemented pattern for building web APIs. Getting the design right makes your API intuitive to use and cheap to maintain. Getting it wrong creates endless confusion for your consumers.
Resources, Not Actions
The central insight of REST is that your API models resources — nouns — rather than actions — verbs. A resource is any piece of data that can be named: a user, an article, a comment, an order. Each resource is identified by a URL, and you interact with it using standard HTTP methods.
The URL structure should look like this:
1 | |
The mistake beginners make is putting verbs in URLs: /getArticles, /createUser, /deleteComment/15. This is wrong. The HTTP method already specifies the action. The URL should identify only the resource. A well-designed REST API feels obvious — you can guess how to access something before reading the documentation.
For nested resources, use the relationship in the URL naturally:
1 | |
Limit nesting depth. Two levels is usually right. If you need deeper nesting — /articles/42/comments/7/likes/12 — the URL becomes unwieldy and suggests you should reconsider your resource design. Some relationships are better expressed through query parameters or separate endpoints.
HTTP Status Codes: Say What You Mean
Status codes communicate the result of a request without the client needing to parse the response body. Used well, they make error handling clean and predictable. Used poorly, they confuse and frustrate.
The ones you need every day:
200 OK— the request succeeded. Use for successful GET, PUT, and PATCH.201 Created— a resource was successfully created. The response should include aLocationheader pointing to the new resource. Use exclusively for POST that creates something.204 No Content— the request succeeded and there is nothing to return in the body. Use for successful DELETE operations.400 Bad Request— the client sent something invalid: malformed JSON, missing required fields, a value that fails validation. The response body should explain what was wrong so the client can fix it without guessing.401 Unauthorized— the request lacks valid authentication credentials. This means “you are not logged in” or “your token is expired.” Provide a clear error message so the client knows whether to redirect to login or refresh a token.403 Forbidden— the credentials are valid but the authenticated user does not have permission for this action. This means “you are logged in, but you are not allowed to do this.” Do not confuse this with 401.404 Not Found— the requested resource does not exist. Be careful about revealing information — returning 404 for a resource the user should not know exists can be a security consideration. Sometimes returning 403 even when the resource does not exist is the safer choice.422 Unprocessable Entity— the request is syntactically valid but semantically wrong. A common use case: creating a user with an email that already exists. The JSON is well-formed and all required fields are present, but the operation cannot proceed because of a business rule.500 Internal Server Error— something unexpected went wrong on the server. This should be rare. If a 500 happens, it is a bug, and you should log the full stack trace for investigation.
The worst API responses are the ones that return 200 OK with a body of { "error": "something went wrong" }. This forces every client to parse successful responses looking for errors. Use the status line for what it is designed for.
Versioning from Day One
APIs change. Fields get renamed, endpoints get moved, and behaviors get modified. When you make a breaking change, existing clients break unless you have a versioning strategy. Implement versioning before you need it, because adding it retroactively is always harder than doing it from the start.
The most common and most transparent approach is URL versioning: /v1/articles, /v2/articles. The version is visible in every request, easy to route in reverse proxies, and trivial to test in a browser. The downside is that it technically violates REST purism — the URL should identify the resource, not the API version — but the practical benefits outweigh the philosophical objection for most teams.
Header-based versioning via the Accept header is REST-pure but harder to test and debug because the version is invisible in URLs and server logs. Query parameter versioning (/articles?version=1) works but pollutes the query parameter namespace. For most teams, URL versioning is the right default choice.
Pagination
If an endpoint can return more than a few dozen items, pagination is not optional. Returning every article in your database because you do not have pagination will eventually bring down your server or your client’s browser.
Use cursor-based pagination when possible, and offset-based pagination when you must. Offset pagination (?page=3&limit=20) is simpler to implement but has a subtle flaw: if a new item is inserted at the top of the list between requests, items shift and the client sees duplicates or misses items. Cursor-based pagination (?cursor=abc123&limit=20) avoids this by pointing to a specific position in the list, but it prevents jumping to arbitrary pages.
Include pagination metadata in your response. The client needs to know if there is more data:
1 | |
This lets clients display “Showing 1-20 of 1,523 results” and know whether to show a “Load More” button.
Filtering, Sorting, and Searching
As collections grow, clients need ways to narrow results:
1 | |
Establish conventions for your query parameters and use them consistently across all endpoints. The convention I use: equals for exact filter (status=published), pipe for multiple values (status=published|draft), comma for range (price=10,50), and a minus prefix for descending sort (-publishedAt for newest first, publishedAt for oldest first).
For full-text search, add a dedicated search endpoint rather than overloading the collection endpoint with a generic ?q= parameter. GET /articles/search?q=docker+containers separates the search concern from filtering and sorting, and the response format for search results often differs from a standard collection response.
Rate Limiting
Without rate limiting, a single misbehaving client — or a bug in your own frontend — can overwhelm your API. Implement rate limiting early and communicate limits clearly through response headers:
1 | |
A reasonable default for authenticated endpoints is 100 requests per minute per user. Unauthenticated endpoints should be more restrictive. When a client exceeds the limit, return 429 Too Many Requests with a Retry-After header indicating how many seconds to wait.
Rate limiting is also a security measure. It slows down brute force attacks on login endpoints, credential stuffing attempts, and scraping bots. A well-configured rate limiter is one of the cheapest security improvements you can make.
Documentation
An API without documentation is an API that will not be used. At minimum, your documentation should describe every endpoint, every parameter, every possible status code, and include example requests and responses. OpenAPI (formerly Swagger) is the standard specification format, and tools like Swagger UI and Redoc generate human-readable documentation from an OpenAPI spec file.
Write the OpenAPI spec as you build the API, not after. The spec serves triple duty: as a design document before implementation, as a contract for frontend and backend teams to agree on, and as generated documentation after launch. Keeping it in sync with the code is the challenge. Libraries exist for most frameworks to generate OpenAPI specs from code annotations, which helps prevent drift.
A well-designed REST API is one of the most durable artifacts a software team can produce. The HTTP methods and status codes have been stable for decades, the conventions are understood by developers in every language, and a clean resource model outlasts the framework it was built with. Invest in getting the design right up front, and your API will serve its users well for years.