Skip to content
RungsySign in

REST API Design

Resources, not verbs — designing an interface another team can use without asking you questions.

45 minDifficulty 2/5api · designReviewedRead and accepted by a person.

Before this

Why this exists

Two teams design the same API. One produces `/getUserOrders`, `/updateUserOrder`, `/deleteOrderById`. The other produces `/users/42/orders` with GET, PATCH and DELETE. The second team's API can be guessed at without documentation, cached by infrastructure they didn't configure, and extended without inventing new verbs.

The mental model

A filing cabinet, not a control panel. You don't press labelled buttons; you locate a folder by its address and then read, replace or remove it. The verbs are fixed and universal — only the addresses change.

How it works

URLs name things; methods do things

This is the central discipline. A URL identifies a *resource* — a noun. The HTTP method supplies the verb. `/orders/42` is one order, and what you do to it is GET, PATCH or DELETE. As soon as a verb appears in the path (`/orders/42/delete`), you've moved the action into the noun and lost everything the method was giving you: caching, safe retries, and a client's ability to predict your API.

Collections and members, consistently

Two shapes cover almost everything. A **collection** — `/orders` — supports GET to list and POST to create. A **member** — `/orders/42` — supports GET to read, PUT or PATCH to change, DELETE to remove. Nest when the child genuinely belongs to the parent: `/users/42/orders`. Don't nest deeper than that; `/users/42/orders/7/items/3/tags` is a path that has stopped helping anyone. Plural nouns throughout, because `/order/42` and `/orders` in the same API is a decision you'll regret.

Statelessness is a design constraint, not an accident

Each request carries everything needed to serve it. The server holds no per-client conversation state between calls. This is what lets any instance serve any request, which is what makes scaling out trivial. It also forces things like pagination to be explicit — a cursor in the request, not a server-side position — which happens to be more robust anyway.

Not everything is a resource, and forcing it makes things worse

"Send this campaign." "Retry this job." "Convert this currency." These are genuinely actions, and contorting them into resources produces APIs nobody enjoys using. The honest answer is a POST to an action endpoint — `POST /campaigns/42/send`. Purists will object. The alternative, inventing a `/campaign-send-requests` collection so you can POST to it, is worse: it adds a fictional entity to your domain to satisfy a rule. **Be RESTful where it helps and pragmatic where it doesn't** — the goal is an API that's predictable, not one that passes a purity test.

The mechanism

The full surface for one resource, with the responses that go with it: | Request | Success | Notes | |---|---|---| | `GET /orders?limit=20&cursor=abc` | 200 | Paginated. Always paginate collections. | | `POST /orders` | 201 + `Location` | Body is the created resource. | | `GET /orders/42` | 200 / 404 | | | `PATCH /orders/42` | 200 | Partial. Omitted fields untouched. | | `PUT /orders/42` | 200 | Full replace. Omitted fields cleared. | | `DELETE /orders/42` | 204 | Idempotent — 204 even if already gone. | Filtering, sorting and pagination belong in the query string, because they're modifiers on the same collection, not different resources.

flowchart LR
    A["/orders"] -->|GET| B[list, paginated]
    A -->|POST| C[201 + Location]
    D["/orders/42"] -->|GET| E[one order]
    D -->|PATCH| F[partial update]
    D -->|PUT| G[full replace]
    D -->|DELETE| H[204]
    I["/users/7/orders"] -->|GET| J[that user's orders]
Diagram source for REST API Design.

What people get wrong

REST means returning JSON over HTTP.
REST is a set of architectural constraints — resource identification, uniform interface, statelessness. JSON is just a common payload format. An API with a single /api endpoint taking a JSON action name is not REST, however much JSON it moves.
Every collection response should return all matching records.
Always paginate. Always cap the page size, including when the client asks for more. The dataset that fits in memory today won't in a year, and an unbounded limit parameter is a denial-of-service vector.
Nesting resources deeply shows the relationships clearly.
One level of nesting is usually the limit. Beyond that, use the flat resource with a query filter. Deep paths are hard to route, hard to cache, and force clients to know the full hierarchy just to fetch one thing.
Versioning can wait until we need it.
Put /v1 in the path from day one. Adding a version later requires either breaking every client or maintaining an unversioned legacy surface forever. It costs nothing upfront.

When not to use it

Clients need wildly different shapes of the same data, and you're either over-fetching or building a dozen bespoke endpoints.
GraphQL. Its cost is losing HTTP caching and inheriting N+1 problems, so make that trade deliberately.
Internal service-to-service calls where latency and payload size dominate.
gRPC. Binary encoding and generated clients beat JSON over HTTP when both ends are yours.
The domain is genuinely event-driven — things happened, rather than things exist.
An event stream. Modelling an event log as CRUD resources fights the domain.

Terms

Resource
Anything worth naming with a URL — an order, a user, a collection of them.
Collection
A resource holding others. Supports GET to list and POST to create.
Member
One item within a collection, addressed by id.
Uniform interface
The REST constraint that the same small set of methods works across every resource.
Cursor pagination
Paging with an opaque pointer to the last item seen, rather than a numeric offset. Stable when rows are inserted mid-listing.
HATEOAS
Responses embedding links to related actions. The most-skipped REST constraint, and rarely worth it for a private API.

In an interview

Design a REST API for a blog with posts and comments.

  • /posts and /posts/:id for the collection and member
  • /posts/:id/comments for a nested collection that genuinely belongs to the post
  • correct methods and statuses — 201 with Location on create, 204 on delete
  • pagination on every collection, with a capped page size
  • versioning in the path from the start

When would you not use REST?

  • GraphQL when clients need varied shapes and over-fetching is the real cost
  • gRPC for internal service-to-service traffic
  • event streaming when the domain is events rather than entities
  • acknowledges each trade: GraphQL loses HTTP caching, gRPC loses browser-native support

Can you recall it?

What's the core discipline of REST, and what do you lose when you put a verb in the URL?

Sources

Keep track of this

Add API Design to your map and Rungsy will schedule reviews so you actually remember it.