AlgoInsight

System Design

Mastering REST API Design: A Practical Guide

Anyone can throw together some HTTP endpoints. Designing a REST API that's clean, consistent, and a pleasure to use is a craft. Here are the principles and patterns that separate the two.

By Admin2026-06-1814 min readrestapi-designhttpbackendsystem-designbest-practices

A REST API is a contract that thousands of other systems may depend on, and once people build against it, changing it is painful. That makes API design one of the highest-leverage skills in backend engineering: a well-designed API is intuitive, predictable, and survives years of growth; a poorly designed one becomes a source of confusion and breakage that's expensive to fix.

This guide covers the principles and patterns that make a REST API genuinely good - resource naming, correct use of HTTP methods and status codes, versioning, pagination, filtering, error handling, and more. These aren't arbitrary rules; each exists because it makes the API easier to understand and harder to misuse.

The core principle: design around resources

REST organizes everything around resources - the "nouns" of your system, like users, orders, or products. The single most important design decision is identifying your resources clearly and giving each a consistent URL structure. Get this right and the rest of the design tends to fall into place.

The golden rule: URLs name resources (nouns), HTTP methods describe actions (verbs). You don't put verbs in your URLs - the method already supplies the verb.

✅ Good:  GET /users/42          (get user 42)
✅ Good:  POST /users            (create a user)
✅ Good:  DELETE /users/42       (delete user 42)

❌ Bad:   GET /getUser?id=42
❌ Bad:   POST /createUser
❌ Bad:   POST /deleteUser/42

The bad examples put the action in the URL, which duplicates what the HTTP method already says and breaks the consistent pattern. When every endpoint follows the noun-plus-method convention, a developer who learns one part of your API can predict the rest.


Use HTTP methods correctly

Each HTTP method has a defined meaning, and using them correctly makes your API predictable:

Method Meaning Idempotent? Safe?
GET Retrieve a resource Yes Yes (no changes)
POST Create a new resource No No
PUT Replace a resource entirely Yes No
PATCH Update part of a resource No* No
DELETE Remove a resource Yes No

Two properties here matter deeply. Safe methods (GET) don't change anything - calling them has no side effects. Idempotent methods produce the same result whether called once or many times: deleting user 42 twice leaves the same end state (deleted). These properties aren't academic - they tell clients, caches, and proxies what's safe to retry or cache. A GET can be cached and safely retried; a POST cannot, because each call creates something new.

Tip: Respecting safety and idempotency is what lets the wider web work with your API. Caches assume GET is safe to store. Clients assume PUT and DELETE are safe to retry after a network failure. Violating these expectations - say, making a GET request change data - breaks assumptions throughout the stack and causes subtle, hard-to-debug problems.


Name resources well

Good resource naming follows a few consistent conventions:

Consistency across all of this is what makes an API feel coherent. If you nest in one place and flatten in another, or pluralize some collections and not others, the API becomes unpredictable and developers have to look up every endpoint instead of guessing correctly.


Return the right status codes

Status codes tell the client what happened without parsing the response body. Using the correct one is a mark of a well-designed API:

Code When to use it
200 OK Successful GET, PUT, or PATCH
201 Created Successful POST that created a resource
204 No Content Successful request with nothing to return (e.g. DELETE)
400 Bad Request The request was malformed or invalid
401 Unauthorized Authentication is missing or invalid
403 Forbidden Authenticated, but not allowed to do this
404 Not Found The resource doesn't exist
409 Conflict The request conflicts with current state (e.g. duplicate)
429 Too Many Requests The client hit a rate limit
500 Internal Server Error Something broke on the server

The most common mistake is returning 200 OK for everything and burying the real outcome in the response body - forcing every client to parse the body to know whether the request even succeeded. Using accurate status codes lets clients handle outcomes correctly with simple, standard logic. Distinguishing 401 (you're not logged in) from 403 (you're logged in but not allowed) and 404 (it doesn't exist) gives callers exactly the information they need to respond appropriately.


Handle errors clearly

When something goes wrong, a good API returns both the right status code and a helpful, structured error body so the developer knows what happened and how to fix it:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "The 'email' field must be a valid email address.",
    "field": "email"
  }
}

A consistent error format across your entire API means clients can handle errors uniformly. Vague errors like {"error": "something went wrong"} force developers to guess, while precise ones turn debugging from an investigation into a quick fix. Be specific about what was wrong and, where possible, how to correct it.


Version your API

Your API will need to change, but existing clients depend on its current behavior - so you can't just change it underneath them. Versioning solves this by letting old and new versions coexist. The most common approach puts the version in the URL path:

/v1/users/42
/v2/users/42

When you need to make a breaking change, you introduce /v2, and clients migrate when they're ready while /v1 keeps working. The key discipline is distinguishing breaking changes (removing a field, changing a response shape) from non-breaking ones (adding a new optional field). Non-breaking changes can go into the current version safely; only breaking changes require a new version.

Warning: Breaking an API without versioning is one of the fastest ways to lose the trust of the developers who depend on you. Once external systems build against your API, a silent breaking change can take down their applications without warning. Version deliberately, communicate changes, and deprecate old versions gradually rather than abruptly.


Paginate large collections

An endpoint like GET /users that returns every user works fine with a hundred users and falls over with ten million. Returning huge collections is slow, wastes bandwidth, and can crash clients. Pagination breaks large result sets into manageable pages:

GET /users?page=2&limit=50      (offset-based: page 2, 50 per page)
GET /users?cursor=abc123&limit=50   (cursor-based: items after a cursor)

Offset-based pagination (page and limit) is simple and intuitive. Cursor-based pagination (a pointer to where you left off) performs better on very large datasets and handles items being added or removed mid-pagination more gracefully. Either way, never return an unbounded collection - always paginate, and document the default and maximum page sizes.


Support filtering, sorting, and field selection

Clients rarely want everything - they want a specific slice. Query parameters let you provide that flexibility without creating dozens of specialized endpoints:

GET /products?category=books&sort=price&order=asc
GET /products?fields=id,name,price        (return only these fields)

Filtering (category=books), sorting (sort=price), and field selection (fields=...) let one well-designed endpoint serve many needs. Field selection is especially useful - it lets clients reduce over-fetching by requesting only the fields they need, addressing one of REST's classic weaknesses without switching to GraphQL.


Keep it stateless

A core REST principle: each request must contain everything the server needs to handle it. The server doesn't remember previous requests or keep per-client session state between calls. Authentication, for instance, is sent with every request (typically a token in the headers) rather than relying on a remembered session.

Statelessness is what makes REST APIs scale so well horizontally: because no server holds client-specific state, any server can handle any request, and you can add servers freely behind a load balancer. It also makes the system more resilient - if a server fails, another picks up the next request seamlessly, because there was no session tied to the failed one.

Interview: When asked to design a system, the quality of your API design is part of what's evaluated. Demonstrating these principles - clean resource naming, correct HTTP methods, sensible status codes, pagination, versioning - shows you think in terms of clear contracts. Even naming a couple unprompted ("I'd paginate this endpoint and version the API from the start") signals professional-grade thinking.


Secure your API

A well-designed REST API controls who can access what. A few standard patterns:

Security isn't a layer you add at the end; it's woven into the design. Deciding your authentication scheme and access model early shapes the whole API.

Make writes safe to retry

Networks fail, and clients retry. If a client sends a POST /payments request, gets no response (because the response was lost, not because the operation failed), and retries, you could create two payments. The solution is idempotency - designing write operations so that repeating them has no additional effect. A common pattern is the idempotency key: the client sends a unique key with the request, and the server uses it to recognize and ignore duplicates. This is essential for any operation where a duplicate would cause harm, and it's a frequent topic in both API design and distributed-systems discussions.

Handle long-running operations gracefully

Not every operation completes instantly. If a request kicks off work that takes seconds or minutes - generating a large report, processing a video - don't make the client wait with the connection held open. The REST-friendly pattern is to respond immediately with 202 Accepted and a way to check on progress: a URL the client can poll, or a webhook the server calls when the work finishes. This keeps the API responsive and avoids timeouts on long jobs, while still giving the client a clear path to the eventual result.

Design for the consumer, not the database

A subtle but important principle: a REST API should reflect how clients think about your domain, not how your database happens to be structured. It's tempting to expose your tables directly - a /user_addresses endpoint because there's a user_addresses table - but that leaks internal structure and forces clients to understand your schema. Instead, design endpoints around the resources and operations that make sense to the people using the API. The internal storage can change freely while the API contract stays stable. This separation is what lets you refactor your database without breaking every client, and it's a hallmark of an API designed with empathy for its consumers.

Document everything

An API is only as good as its documentation, because developers can't use what they can't understand. Good documentation describes every endpoint, its parameters, example requests and responses, the possible status codes, and the authentication required. Many teams use standards like OpenAPI (formerly Swagger) to describe their API in a machine-readable format, which can generate interactive documentation and client libraries automatically. Whatever the format, treat documentation as part of the product, not an afterthought - a brilliantly designed API with no docs is nearly unusable.

Consistency is the meta-principle

If there's a single thread running through everything above, it's consistency. The specific conventions matter less than applying them uniformly: pick a naming style and use it everywhere; choose an error format and return it from every endpoint; decide how you paginate and paginate the same way throughout. An API that's internally consistent can be learned once and used everywhere - a developer who understands one endpoint can correctly guess the rest. An inconsistent API, even one that follows best practices in places, forces developers to check the documentation for every single call. Consistency is what turns a collection of endpoints into a coherent, predictable interface.


A quick checklist

A well-designed REST API tends to satisfy all of these:

None of these is complicated on its own. Applied consistently across an entire API, they produce something developers can learn once and use everywhere - which is the whole goal. A good REST API feels almost inevitable to use: you can guess the endpoint you need, predict the response shape, and know what a given status code means without checking the docs. That predictability is what these principles, applied together, create. We work through API design as part of building real systems across System Design Fundamentals.


Go deeper

More articles