If you have ever tried to glue two web services together and found yourself in a swamp of inconsistent URLs, weird status codes, and ambiguous error messages, you already know why REST API design matters. A good API is a joy to use. A bad one wastes hours of every consumer's life, every day, forever. This article is the practical guide we wish someone had handed us when we started designing APIs — the patterns that work, the mistakes that bite, and the small set of rules that will keep you out of trouble.
What REST actually means
REST stands for Representational State Transfer. It is an architectural style, not a protocol. The core idea: your API exposes resources (articles, users, orders), and clients interact with them using standard HTTP verbs. REST is the dominant style for public web APIs because it maps cleanly onto HTTP and is easy to reason about.
You will sometimes hear people argue about whether REST is "broken" and we should all use GraphQL or gRPC instead. There are real trade-offs. For most apps, REST is still the right default. We use REST for almost every public API at Mangobaz.
Resources: nouns, not verbs
The single most important rule in REST: URLs are for resources (nouns), HTTP methods are for actions (verbs). The URL /articles is a collection of articles. The URL /articles/42 is a specific article. The HTTP method says what you are doing with it.
GET /articles— list articles.GET /articles/42— fetch one article.POST /articles— create a new article.PUT /articles/42— replace article 42 entirely.PATCH /articles/42— update some fields of article 42.DELETE /articles/42— delete article 42.
What you should never do: POST /getArticles, GET /createUser. URLs that look like commands betray a misunderstanding of what HTTP is for.
Status codes: the right words for the situation
HTTP status codes are how your server tells the client what happened. They are not just for show — clients use them to decide whether to retry, show an error, or treat the response as success. Use them properly.
- 200 OK — success with a body.
- 201 Created — success, a new resource was made. Return the new resource and a
Locationheader with its URL. - 204 No Content — success with no body (use this for successful DELETE).
- 400 Bad Request — the client sent something malformed. Fixable by the client.
- 401 Unauthorized — the client is not authenticated.
- 403 Forbidden — the client is authenticated but not allowed to do this.
- 404 Not Found — the resource does not exist.
- 409 Conflict — the request would conflict with current state (duplicate email, version mismatch).
- 422 Unprocessable Entity — well-formed but semantically wrong (validation failed).
- 500 Internal Server Error — something went wrong on the server. The client cannot fix this.
The distinction between 400 and 422 is subtle: 400 means the request cannot be parsed, 422 means it parsed but the data is invalid. Most APIs use 422 for validation errors because it lets them return a structured list of field-level problems.
Versioning
Your API will change. New fields will appear. Old endpoints will be retired. To avoid breaking clients in production, version your API:
https://api.example.com/v1/articles
https://api.example.com/v2/articles
URL-based versioning (with a /v1/ prefix) is the most common. Header-based versioning (Accept: application/vnd.example.v2+json) is more correct but rarely used in practice. Pick URL versioning unless you have a strong reason to do otherwise.
Pagination
Do not return 100,000 articles in a single response. Paginate. The two common patterns:
- Offset pagination —
?page=2&limit=20. Simple, easy to understand, but slow for deep pages. - Cursor pagination —
?after=eyJpZCI6MTAwfQ&limit=20. Opaque cursor that points at the last item. Fast and stable.
Use cursor pagination for infinite feeds (Twitter, Reddit). Use offset pagination for admin panels and reports where jumping to page 50 is useful. Always include a total count if it is cheap to compute.
Filtering and sorting
Allow clients to query your list endpoints with simple parameters:
GET /articles?status=published&author=mango&sort=-publishedAt&limit=20
Be consistent across endpoints. Use a leading minus sign for descending order. Reject unknown parameters with a 400, or ignore them silently — pick one and document it.
Error responses: structured, helpful, secure
When something goes wrong, return JSON that tells the client exactly what happened. RFC 7807 defines a useful shape:
{
"type": "https://example.com/errors/validation",
"title": "Validation failed",
"status": 422,
"errors": [
{ "field": "email", "message": "must be a valid email" },
{ "field": "password", "message": "must be at least 8 characters" }
]
}
The type is a URL where the client can read more. The errors array lets the client render inline form errors. Do not leak stack traces or internal paths in production — log them server-side, return a clean message client-side.
Authentication and authorisation
Most public APIs require authentication. The two main approaches:
- API keys — a long random string passed in a header. Easy to revoke, easy to leak. Good for server-to-server.
- OAuth 2.0 / JWT — short-lived tokens issued after login. More complex but safer for user-facing apps. See our JWT article.
Authentication is "who are you?" Authorisation is "are you allowed to do this?" Always check both. Returning 401 when the user is missing credentials and 403 when they are present but lack permission is the canonical pattern.
Idempotency
Some HTTP methods are idempotent by definition: GET, PUT, DELETE. Sending the same DELETE /articles/42 twice has the same effect as sending it once. POST is not idempotent — sending the same POST twice creates two resources.
For POST endpoints that should be idempotent (creating a payment, charging a card), accept an Idempotency-Key header. The client generates a unique key per logical operation. The server stores the key with the result and returns the same response if the same key comes in twice within a window. Stripe's API is the canonical implementation of this pattern.
Documentation
The single highest-leverage thing you can do for your API is document it well. The two standards:
- OpenAPI (formerly Swagger) — a machine-readable spec for your API. Generates client SDKs, mock servers, and interactive docs. Almost every public API publishes one.
- Markdown + examples — a simple
README.mdwith example requests and responses. Faster to write, less tooling required.
Write the docs as you build the API, not after. If you cannot describe an endpoint clearly, the design is probably wrong.
Common pitfalls
- Returning the wrong status code. Always think about what the client should do next. 200 with a body containing
"error": "..."is the worst — the client cannot tell success from failure without parsing the body. - Breaking changes without versioning. Adding a required field, changing a response shape, removing an endpoint — all of these break existing clients. Always version first.
- Not validating input. Validate every field on the server, even if the client also validates. Clients can be wrong, and direct API calls bypass the form.
- Returning too much data. Never return internal IDs, password hashes, or stack traces in production responses.
- Inconsistent naming. Pick camelCase or snake_case and stick with it across every endpoint. Mixing styles is a small detail that adds up.
A more advanced thing: rate limiting and quotas
Any public API needs rate limiting — without it, a buggy client can take down your service or a m
Further reading
REST design is half convention and half specification. For the specification side we trust IETF; for the practical API surface we trust MDN. For the architectural intent behind REST, Roy Fielding’s dissertation is the original source.
- MDN HTTP methods reference — the reference for every HTTP method, including which are safe, which are idempotent, and which are allowed to have request bodies.
- IETF RFC 9110 — HTTP Semantics — the current IETF standard that defines HTTP semantics, including methods, status codes, headers, and caching.
- Roy Fielding’s dissertation (REST) — the chapter from Fielding’s 2000 PhD dissertation that defined the Representational State Transfer architectural style.
Most cloud providers and gateways (Cloudflare, AWS API Gateway, Kong) handle this for you. The response should include headers like Retry-After and X-RateLimit-Remaining so well-behaved clients can self-throttle. Stripe's API documentation is the gold standard for how to communicate these headers clearly.
Caching with ETag
If a client fetches an article, they often re-fetch the same article later. Save them the round-trip with HTTP caching:
GET /articles/42
ETag: "v1-abc123"
Cache-Control: max-age=60
GET /articles/42
If-None-Match: "v1-abc123"
→ 304 Not Modified (no body)
Generate the ETag from a content hash or version number. The client sends the ETag back; if the resource has not changed, the server returns 304 with no body. Saves bandwidth and CPU. For list endpoints, caching is harder — usually leave it to the client.
FAQ
What is the difference between REST and GraphQL?
REST exposes many endpoints, each returning a fixed shape. GraphQL exposes one endpoint, and the client specifies the shape of the response. GraphQL is great for apps with complex, varying data needs. REST is simpler and easier to cache. Most apps do not need GraphQL.
What is the difference between PUT and PATCH?
PUT replaces the resource entirely. PATCH applies a partial update. If a field is not in the PATCH body, it stays unchanged. With PUT, missing fields are typically cleared.
Should I use plural or singular nouns in URLs?
Plural. /articles/42, not /article/42. The resource is one of many. Singular feels right for the URL but quickly becomes inconsistent when you have multiple related resources.
How do I version my API?
URL versioning is simplest: /v1/articles, /v2/articles. Document the differences clearly. Never silently change behaviour of an existing version.
How do I handle file uploads?
Two options: send the file as multipart/form-data with a POST, or upload directly to S3/Cloud Storage and send only the URL in your API. The second is better for large files because it does not hold up your server thread.
What is HATEOAS?
Hypermedia As The Engine Of Application State. The idea is that responses include links to related resources, so the client can navigate the API without hardcoding URLs. In practice, almost nobody does this. REST without HATEOAS is still REST.
Homework
Design a REST API for a tiny bookmarking service. Users can save URLs with tags, list their bookmarks, search by tag, and delete. Write down:
- The endpoints (URL + method).
- The request and response shapes for each (JSON examples).
- The status codes returned in every case, including errors.
- The pagination strategy.
- The authentication model.
Then build it with ASP.NET Core minimal APIs (see our Razor Pages article) or Express, and document it with OpenAPI. By the end you will have a working API, with documentation, that other people can consume without ever talking to you.