Skip to main content

Conceptintermediate

REST

Overview

REST is an architectural style based on resources identified by URL, manipulated through a fixed set of verbs, with semantics defined by HTTP itself.

Almost no API called REST is REST in the original sense — most are "HTTP with JSON", which is legitimate and is not the same thing.

The distinction matters less as purity and more for what you lose by ignoring it: caching, idempotency, uniform error handling and evolution — all of which the protocol already offers, for free, to whoever uses its semantics.

Problem

The most common pattern in HTTP APIs is tunneling method calls:

POST /createOrder
POST /getOrderById
POST /cancelOrder

That works, and throws away what the protocol gives. Everything is a POST, so nothing is cacheable or safe to retry. Errors become a 200 with a success: false field, so no intermediary — proxy, gateway, client — understands what happened.

The result is an API that has to reimplement, in its own convention, things HTTP already solves.

Core Concepts

A resource, not an operation

The central modeling: the URL identifies things, and the verb says what you do with them.

GET /orders/123 fetch
PUT /orders/123 replace
PATCH /orders/123 partially change
DELETE /orders/123 remove
POST /orders create

The guiding question: if this were a document, what would its address be?

Actions that do not fit a noun — cancel, approve, reprocess — usually reveal a hidden resource. "Cancel order" can be POST /orders/123/cancellation: the cancellation is a thing, with a date, a reason and an author.

That is not word play. A cancellation as a resource has identity, can be queried and audited — which the POST /cancelOrder operation does not.

Safety and idempotency come from the verb

The most useful property and the most ignored:

GET, HEAD safe and idempotent — does not change; can be retried, cached, prefetched
PUT, DELETE idempotent — repeating has the same effect
POST neither — repeating creates again
PATCH neither — depends on what you send

That is not decorative convention: proxies, browsers, clients and service meshes act on those guarantees. A GET that changes state will be repeated by a prefetch. A POST with no idempotency key will be duplicated by a retry.

See idempotency.

The status code is part of the contract

200/201/204 success, with or without a body
400 the request is wrong — retrying will not help
401/403 unauthenticated / unauthorized
404 does not exist
409 state conflict
422 valid syntax, invalid semantics
429 too many requests — retry later
5xx server error — retrying may work

The split between 4xx and 5xx is what lets the client decide whether to retry. An API that returns a 200 with an error in the body takes that decision away from the caller, and forces every client to reimplement the classification.

What almost nobody does: hypermedia

In the original REST, the response carries links to the next possible actions, and the client navigates them instead of constructing URLs.

The promise is decoupling the client from the address structure and communicating state transitions — a paid order carries a refund link; a pending one does not.

Adoption is very low, and it is worth being honest about why: most clients are written against a specification and gain nothing from discovering links at runtime. The cost appears immediately, the benefit rarely.

Where it pays: long-lived public APIs, with many clients the provider does not control.

Versioning: prefer not to

See schema evolution. Adding a field is free; adding a resource is free. Most of an HTTP API's evolution fits in a compatible change.

Versioning in the URL is the most common and most visible form — and each live version is code to maintain.

Pagination, filtering and sorting are contract

Large collections need pagination, and the choice has a consequence:

By offset — simple, and the page changes if records are inserted during navigation.

By cursor — stable under insertion, and it does not allow jumping to an arbitrary page.

Whichever it is, it has to be in the contract — including the maximum limit and what happens when you ask for more.

Mental Model

REST is using HTTP's semantics, not going around them. Every ignored guarantee is one you will reimplement worse.

When to Use

  • The client needs the response to continue.
  • The model is naturally one of resources with a lifecycle.
  • Diverse consumers, including browsers.
  • HTTP caching has value.
  • A public API, where universal tooling matters.
  • Operational simplicity weighs more than transport efficiency.

When Not to Use

When the consequence is asynchronous. See messaging integration.

When the client needs highly variable fields. See GraphQL.

Very high frequency internal communication. See gRPC — the serialization and connection cost weighs.

Bidirectional or long-lived flows.

Transferring large volumes in batches. See batch integration.

When the operation is not about a resource. Forcing a noun onto computations and complex searches produces tortured modeling — there an operation endpoint is more honest.

Alternatives

  • GraphQL — when consumption is variable.
  • gRPC — internal, high frequency, strong contract.
  • Messaging — asynchronous.
  • Webhooks — to notify instead of being polled.

Trade-offs

RESTgRPC
Universal toolingNeeds support
Human-readableBinary
HTTP cachingNot native
Frequently loose contractStrong by definition
More bytesCompact
RESTGraphQL
Fixed response per resourceThe client chooses
Simple cachingDifficult
Several calls to composeOne
Predictable costA query can be expensive

Failure Modes

Everything via POST. Nothing is cacheable or safe to retry.

A 200 with an error in the body. The client does not know whether to retry.

A GET with a side effect. A prefetch triggers the action.

No idempotency key on creation. A retry duplicates.

A collection with no pagination. One query returns everything and takes both sides down.

A sequence of calls in a loop. The client makes N requests to assemble one screen — the problem that motivates GraphQL.

Common Mistakes

Modeling operations instead of resources.

Not using the status codes.

Not offering an idempotency key on POST.

Versioning by reflex.

Pagination outside the contract.

Exposing the internal database model as a resource. The resource is part of the public contract; the internal model has to be able to change.

Real-World Example

A logistics operator exposed an HTTP API with 40 endpoints, all POST, all returning a 200 with {success: bool, error: string}.

Four consequences, all discovered separately:

Retries duplicating shipments. The mobile client retried on network failures. Since everything was a POST with no idempotency key, each retry created a new shipment. About 300 duplicated shipments per month, handled manually by support.

No caching. The shipment status lookup was the most called endpoint — 40% of the traffic — and could not be cached because it was a POST. The database absorbed everything.

Error classification in the application. Each of the eleven clients had its own logic to decide whether the text in the error field was retryable. Three were wrong, and retried indefinitely on permanent errors.

A useless gateway. The gateway could not apply rate limits per operation type, nor cache, nor report error rates — because everything was a POST with a 200.

The migration was done in parallel, with the old API kept for fourteen months.

Lookups became GET with a 30-second cache. Database traffic dropped 60%.

Real status codes. The clients deleted their own classification and came to use 4xx versus 5xx.

An idempotency key required on creation. The duplicates went to zero.

Resources instead of operations. POST /shipments/{id}/cancellation replaced POST /cancelShipment. The cancellation became queryable, which solved an old audit demand nobody had connected to it.

What was recorded afterwards: none of the four fixes was about stylistic purity. Each one removed code that existed only to compensate for a protocol guarantee that was being wasted.

Practical Exercise

Take your team's API and count how many endpoints are POST. For each one, ask: does this change state?

The ones that do not should be GET — and each one is caching and safe retries you are leaving on the table.

Interview Questions

  • Why does the distinction between 4xx and 5xx matter to the client?
  • What do you lose by doing everything via POST?
  • How do you model "cancel order" as a resource, and what do you gain from it?

Further Reading

  • Fielding, Roy. Architectural Styles and the Design of Network-based Software Architectures. Doctoral dissertation, 2000.
  • Richardson, Leonard; Amundsen, Mike. RESTful Web APIs. O'Reilly, 2013.
  • Allamaraju, Subbu. RESTful Web Services Cookbook. O'Reilly, 2010.
Finished reading this document?Your progress is saved in this browser only.