REST is a style, not a specification
REST — Representational State Transfer — was named and described by Roy Fielding in chapter five of his 2000 doctoral dissertation at UC Irvine. Fielding had co-authored the HTTP/1.1 specification, and the dissertation was in large part a retrospective explanation of why the web scaled: what architectural properties made it possible for a system with no central coordinator to grow by six orders of magnitude without being redesigned.
That origin matters because it tells you what kind of thing REST is. There is no REST specification with a
version number, no conformance suite, no Content-Type: application/rest. There is a set of
constraints, and an architecture either satisfies them or does not. Fielding derived the style by starting
from the null style — anything goes — and adding constraints one at a time, each buying a specific
property: scalability, independent evolvability, visibility to intermediaries, reduced latency.
The word RESTful is the adjective people reach for when an API mostly follows the style. In everyday use it has drifted a long way from Fielding's definition: it now usually means "resource-shaped URLs, JSON bodies, and HTTP methods used roughly correctly". That is a perfectly useful thing to mean, and this guide will use the common sense of the word while being explicit about where it diverges from the formal one.
The six constraints
1. Client-server
Separate the user interface from the data store. The client knows nothing about how orders are persisted; the server knows nothing about whether the caller is an iOS app, a React page or a cron job. The benefit is independent evolution: you can rewrite the storage layer from Postgres to a sharded cluster without shipping a new mobile build, as long as the representations on the wire stay the same. This constraint is the reason an API is worth having at all.
2. Stateless
Each request from client to server must contain everything needed to understand it. The server keeps no
client context between requests. Concretely: no server-side session that request two depends on request one
having created. If a request needs identity, it carries the credential —
typically an Authorization header holding a
bearer token — on every single call.
3. Cacheable
Responses must declare themselves cacheable or not, so that a client or intermediary can reuse a response
rather than repeating the request. On HTTP this is Cache-Control, ETag and
Last-Modified. A well-cached read path is often the single largest performance win available:
a client that sends If-None-Match and receives
304 Not Modified pays a round trip but no bandwidth and no database
query. The HTTP headers guide covers the directives in detail.
4. Layered system
A client cannot tell whether it is connected to the origin server or to an intermediary. That is what lets you slide a CDN, a load balancer, an API gateway, a WAF or a caching proxy into the path without any client changing. The cost is that each layer only sees what is visible in the message, which is precisely why the other constraints insist on putting meaning in the method, the URI, the status code and the headers rather than hiding it in the body.
5. Uniform interface
The central constraint, and the one that makes REST recognisable. Fielding breaks it into four sub-constraints: resources are identified by URIs; resources are manipulated through representations; messages are self-descriptive (the method and media type tell you how to process them); and hypermedia drives application state. It is a deliberate trade: a generic, uniform interface is less efficient than an interface tailored to one client's exact needs, but it means every cache, proxy, browser, log aggregator and testing tool on earth already understands your API.
6. Code-on-demand (optional)
A server may extend a client by sending executable code — JavaScript to a browser being the everyday example. This is the only constraint Fielding marked optional, and API developers essentially never use it. Mention it mostly so you recognise the list when you see it.
Resources, URIs and representations
A resource is any concept worth naming: an order, a customer, the collection of open
orders, today's sales report. The URI identifies the resource. The HTTP method says what to do to it. That
division is the whole trick, and it is why GET /getUser?id=5 is not REST — it smuggles the
verb into the identifier, leaving the actual entity with no stable name. Two clients asking for the same
user via /getUser?id=5 and /fetchUserById?userId=5 would be talking about one
thing under two identities, and no cache could tell.
The conventional shape is plural collection nouns with item identifiers beneath them:
| URI | Identifies | Typical methods |
|---|---|---|
/v1/orders | The collection of orders | GET, POST |
/v1/orders/42 | One order | GET, PUT, PATCH, DELETE |
/v1/orders/42/items | Line items of order 42 | GET, POST |
/v1/orders/42/items/3 | One line item | GET, PATCH, DELETE |
/v1/customers/7/orders | Orders belonging to customer 7 | GET |
Plural naming is a convention rather than a rule, but consistency is not optional: an API where some
collections are /orders and others /customer forces every consumer to keep the
documentation open. Nest sub-resources only where the child genuinely has no meaning outside the parent.
Three levels of nesting is usually a sign that something deserves its own top-level collection with a
filter, so prefer /v1/items?order_id=42 over
/v1/customers/7/orders/42/items once the hierarchy stops earning its keep.
A representation is not the resource — it is a snapshot of the resource's state in some
format at some moment. "Order 42" is a concept in your domain; the JSON document you receive is one
representation of it, and an HTML page or a CSV row could be another. The client and server negotiate which
one using Accept on the request and Content-Type on the response. JSON (RFC 8259)
is the de facto default for web APIs, but nothing in REST requires it, and this distinction is exactly what
makes it possible to add a new format later without breaking existing clients.
The uniform interface applied
With resources named as nouns, the methods carry all the operational meaning. The mapping below is the one most APIs use; the HTTP methods reference covers the full semantics including safety, idempotence and cacheability.
| Method | On a collection | On an item | Typical success | Typical failure |
|---|---|---|---|---|
GET | List | Read | 200 | 404 |
POST | Create | Rarely used | 201 + Location | 422, 409 |
PUT | Replace whole collection (rare) | Replace item | 200 or 204 | 409, 412 |
PATCH | Not used | Partial update | 200 | 422, 415 |
DELETE | Rarely used | Remove item | 204 | 404, 409 |
The PUT versus PATCH choice trips up most teams, because a partial body sent with
PUT will silently wipe fields on a compliant server; the
PUT vs PATCH guide works through when each is right. Note also that
POST is the deliberate escape hatch for operations that are genuinely not CRUD —
POST /v1/orders/42/refunds models a refund as a resource being created, which is more honest
than inventing POST /v1/refundOrder.
Status codes are the machine-readable outcome channel, and using them properly is what lets generic
middleware work. A client library can retry a 503, refresh a token on
a 401, and surface a
400 to the developer, all without knowing a thing about your domain.
Bury the outcome in a {"success": false} field under a
200 and every one of those layers goes blind. The full list lives in
the HTTP status code reference.
Statelessness in practice
Statelessness is the constraint most often misunderstood, usually as "a REST API cannot store anything". It can. The database is server state, not client state, and REST has no objection to it. What the constraint forbids is client session state held on the server between requests — the pattern where request two only makes sense because request one left something in memory on that particular machine.
| Pattern | Allowed? | Why |
|---|---|---|
| A database of orders and customers | Yes | Resource state, not session state |
A token sent in Authorization on every request | Yes | The request is self-contained |
| Server-side cart held in process memory, keyed by session | No | Later requests depend on earlier ones landing on the same box |
A cart stored as /v1/carts/9 in the database | Yes | The cart is now an addressable resource |
| Multi-step wizard where step 3 needs step 2 in memory | No | Session affinity; breaks on any redeploy |
| A cache in front of the origin | Yes | Intermediaries are invisible to the client |
The payoff is horizontal scaling. If any request can be handled by any instance, you scale by adding instances behind a load balancer and you survive a node dying mid-conversation. Sticky sessions, by contrast, turn a rolling deploy into a user-visible incident and make load balancing a matter of luck. Statelessness has a real cost — every request repeats its authentication and context, so requests are larger and the server re-derives things it just derived — and that cost is what caching is there to offset.
HATEOAS and the Richardson Maturity Model
The fourth sub-constraint of the uniform interface is hypermedia as the engine of application
state, usually abbreviated HATEOAS. The idea is that a response tells the client not just the
current state but what it can do next, as links. Fetch an order and the representation includes a
cancel link only while cancellation is actually possible, so the client never needs to encode
the business rule about which states allow cancellation:
{"id":42,"status":"pending","_links":{"self":{"href":"/v1/orders/42"},"cancel":{"href":"/v1/orders/42/cancellation"}}}
Leonard Richardson's maturity model is a useful ladder for talking about how far an API goes:
| Level | Name | What it looks like |
|---|---|---|
| 0 | The swamp of POX | One endpoint, one method. POST /api with an action name in the body |
| 1 | Resources | Many URIs, but still mostly POST to each |
| 2 | HTTP verbs | Methods and status codes used with their real meanings |
| 3 | Hypermedia controls | Responses carry links describing available transitions |
Be clear-eyed about where the industry sits: nearly every API described as REST — including those from Stripe, GitHub and the major clouds — is level 2. Level 3 is rare, and its rarity is not simply laziness. Hypermedia pays off when clients are numerous, unknown and long-lived, because it lets URLs move without breaking anyone. It pays much less when there are three known clients written against generated SDKs, all of which hardcode the URL templates anyway, and it costs payload size, client complexity and a media type decision (HAL, JSON:API, Siren) that every consumer must learn. Choosing level 2 deliberately is a legitimate engineering trade-off. Just know that you have made it, and do not claim strict RESTfulness in a design review you will lose.
Practical design decisions
Versioning
Two mainstream options. URL path versioning — /v1/orders, then
/v2/orders — is visible, trivially routable, easy to cache separately and obvious in logs. It
is also, purists point out, wrong: the resource "order 42" did not become a different thing because you
changed its serialisation, yet it now has two URIs. Header versioning keeps one URI and
negotiates the representation, usually with a vendor media type like
Accept: application/vnd.example.v2+json. It is theoretically cleaner and practically more
annoying: harder to try in a browser, easy to forget in a curl command, and it needs a correct
Vary: Accept or your CDN will serve v1 bodies to v2 clients. Most public APIs pick the path.
Whichever you choose, version as rarely as possible. Additive changes — a new optional field, a new endpoint — do not need a version at all if your clients are written to ignore unknown fields, and telling consumers that in your documentation from day one is worth more than any versioning scheme.
Pagination
Offset pagination (GET /v1/orders?limit=25&offset=50) is easy and maps straight to SQL, and
it is subtly broken under concurrent writes. Suppose orders are sorted newest first and a client reads page
1 (rows 1–25), then a new order arrives. Every existing row shifts down one position, so page 2 —
offset=25 — now begins at what was row 24. The client sees row 25 twice and, symmetrically, a
deletion between pages causes a row to be skipped entirely and never seen. Deep offsets are also slow: most
databases must walk and discard all the skipped rows.
Cursor pagination fixes both. The server returns an opaque cursor encoding the sort key of the last row —
GET /v1/orders?limit=25&after=eyJpZCI6MTE4fQ — and the query becomes
WHERE (created_at, id) < (…) ORDER BY created_at DESC, id DESC LIMIT 25, which is index-friendly
at any depth and immune to shifting. The cost is that you lose random access: there is no "jump to page 47".
For an API feeding an infinite scroll or a sync job, that is no loss at all.
Filtering and sorting
Filters belong in the query string, since they select a subset of a collection rather than identifying a
different resource: GET /v1/orders?status=open&created_after=2026-01-01&sort=-total&limit=50.
A leading minus for descending order is a common convention. Whitelist the fields that can be filtered and
sorted — an open-ended filter language over arbitrary columns is a full table scan waiting to happen — and
return 400 for an unknown parameter rather than silently ignoring it,
because silent ignores hide client typos for months.
Error responses
Set the status code first, then give a body that a human debugging at 2am can act on. RFC 9457,
Problem Details for HTTP APIs, standardises that body and obsoleted the earlier RFC 7807. It uses
the media type application/problem+json and defines the members type,
title, status, detail and instance, plus any extensions
you need:
{"type":"https://example.com/probs/insufficient-stock","title":"Insufficient stock","status":409,"detail":"Only 2 units of SKU-77 remain","instance":"/v1/orders/42","sku":"SKU-77"}
Whether or not you adopt the RFC, be consistent: one error shape across the whole API, a stable machine-readable code that clients can branch on, and no stack traces in production. Reserve 422 for a well-formed request that fails validation and 400 for one that is malformed.
Idempotency keys
POST is not idempotent, so a client that times out mid-request cannot safely retry — it has no
way to know whether the order was created. The standard fix is a client-generated
Idempotency-Key header carrying a UUID. The server stores the key with the result of the first
request and, on seeing the key again, returns the original response instead of creating a second order.
Give the keys a retention window, and scope them per endpoint and per caller so two clients cannot collide.
Rate limiting
Publish your limits in response headers so clients can slow down before they are rejected, and answer
excess traffic with 429 Too Many Requests plus a
Retry-After. The API rate limiting guide covers window
algorithms, backoff and the header conventions.
A worked example: a small orders API
Here is the whole style in one place. Create an order by posting to the collection:
POST /v1/orders with Content-Type: application/json,
Authorization: Bearer eyJhbGciOi…, Idempotency-Key: 6f1e… and body
{"customer_id":7,"currency":"GBP","items":[{"sku":"SKU-77","qty":2}]}
The server answers 201 Created with
Location: /v1/orders/42, ETag: "v1" and body
{"id":42,"status":"pending","total":4998,"currency":"GBP","created_at":"2026-07-20T09:14:02Z"}.
Note the total in minor units — floating-point money is a bug you only find in production.
| Request | Response | Notes |
|---|---|---|
GET /v1/orders/42 | 200 + body + ETag | Safe, cacheable, repeatable |
GET /v1/orders/42 with If-None-Match: "v1" | 304, no body | Cheap revalidation |
GET /v1/orders?status=open&limit=25 | 200 + list + cursor | Filtered collection |
PATCH /v1/orders/42 body {"notes":"gift wrap"} | 200 + updated body | Partial update; other fields untouched |
PUT /v1/orders/42 with If-Match: "v1" | 200, or 412 | Optimistic concurrency |
POST /v1/orders/42/refunds | 201 + Location | An action modelled as a resource |
DELETE /v1/orders/42 once shipped | 409 + problem body | Conflict with current state |
DELETE /v1/orders/99 | 404 | No such resource |
| Any request with no token | 401 + WWW-Authenticate | Authentication missing or expired |
Every one of those requests is reproducible in the tester — set the method, paste the URL, add the header.
What REST is not
REST is not "JSON over HTTP". An endpoint at POST /api that takes
{"action":"getUser","id":5} and returns JSON is level 0 of the maturity model — it uses HTTP as
a transport tunnel rather than as an application protocol, and it forfeits caching, method-level
authorisation, idempotent retries and every tool that reads the method and status.
REST is not RPC, and RPC is not a mistake. RPC-style APIs — including gRPC, which uses Protocol Buffers over HTTP/2 — model the interface as a set of procedures rather than a set of resources. That fits some problems better, particularly internal service-to-service calls where you control both ends, want a strict schema and generated stubs, and care about serialisation cost more than about browser reachability or CDN cacheability. gRPC's streaming and its compact binary encoding are genuine advantages REST does not offer.
GraphQL takes a third position: one endpoint, a typed schema, and the client specifying the exact shape of the response. It solves over-fetching neatly and pays for it in caching complexity, error-handling surprises and server-side query cost control. The trade-offs are involved enough to deserve their own page — see REST vs GraphQL rather than a paragraph here. None of the three is a winner in the abstract, and plenty of production systems run all of them: REST at the public edge, gRPC between services, GraphQL for a rich client.
Try it yourself
Reading about resource design is much less useful than watching real responses. Open the
API tester and send GET https://api.github.com/repos/torvalds/linux: you will
get a level-2 REST response with an ETag you can replay as If-None-Match, plus
rate-limit headers. Follow one of the _url fields in that body and you are doing hypermedia
navigation by hand. Then try GET https://api.github.com/repos/torvalds/linux/issues?state=open&per_page=5
to see filtering and pagination together, and look at the Link header for the next-page cursor.
The REST API tester gives you method, headers, auth and body in one pane and shows the raw response alongside a formatted one. From there, the REST API testing guide covers turning ad-hoc requests into a repeatable suite, API authentication covers the credential side, and CORS explains why the same request that works here fails in your browser.
Frequently asked questions
Is REST a protocol or a standard?
Neither. It is an architectural style described in Roy Fielding's 2000 dissertation — a set of constraints, with no specification version or conformance suite. HTTP, whose semantics RFC 9110 defines, is the protocol most REST APIs are built on, but REST itself is protocol-independent.
What are the six constraints of REST?
Client-server, stateless, cacheable, layered system, uniform interface, and code-on-demand. The first five are required; code-on-demand — the server shipping executable code to the client — is explicitly optional and rarely used in APIs.
Why is /getUser?id=5 not RESTful?
It puts the verb in the URL. The URI should identify a resource and the method supply the action, so the
RESTful form is GET /v1/users/5. Verb-in-path URLs leave the entity with no stable identifier,
which breaks caching, linking and method-level authorisation.
Does a REST API have to return JSON?
No. REST says nothing about format. JSON (RFC 8259) is the de facto default, but XML, CSV, Protocol
Buffers and HTML are all valid representations — content negotiation via Accept exists
precisely so one resource can have several.
Do I need HATEOAS for an API to count as REST?
By Fielding's definition yes; without it you are at level 2 of the Richardson Maturity Model. In practice most production APIs are level 2, and for clients written against fixed documentation that is a defensible trade-off rather than a failure.
How should a REST API return errors?
Status code as the outcome, plus a structured body. RFC 9457 Problem Details, which obsoleted RFC 7807,
defines type, title, status, detail and
instance served as application/problem+json. Never return
200 with an error flag inside.