What is a REST API?

REST is an architectural style for distributed systems — a set of constraints, not a protocol, a standard or a library. This guide covers what those constraints actually are, how they map onto HTTP, and the design decisions you will make on any real API: resources, versioning, pagination, errors and idempotency. Every request shown here can be sent from the online API tester as you read.

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:

URIIdentifiesTypical methods
/v1/ordersThe collection of ordersGET, POST
/v1/orders/42One orderGET, PUT, PATCH, DELETE
/v1/orders/42/itemsLine items of order 42GET, POST
/v1/orders/42/items/3One line itemGET, PATCH, DELETE
/v1/customers/7/ordersOrders belonging to customer 7GET

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.

MethodOn a collectionOn an itemTypical successTypical failure
GETListRead200404
POSTCreateRarely used201 + Location422, 409
PUTReplace whole collection (rare)Replace item200 or 204409, 412
PATCHNot usedPartial update200422, 415
DELETERarely usedRemove item204404, 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.

PatternAllowed?Why
A database of orders and customersYesResource state, not session state
A token sent in Authorization on every requestYesThe request is self-contained
Server-side cart held in process memory, keyed by sessionNoLater requests depend on earlier ones landing on the same box
A cart stored as /v1/carts/9 in the databaseYesThe cart is now an addressable resource
Multi-step wizard where step 3 needs step 2 in memoryNoSession affinity; breaks on any redeploy
A cache in front of the originYesIntermediaries 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:

LevelNameWhat it looks like
0The swamp of POXOne endpoint, one method. POST /api with an action name in the body
1ResourcesMany URIs, but still mostly POST to each
2HTTP verbsMethods and status codes used with their real meanings
3Hypermedia controlsResponses 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.

RequestResponseNotes
GET /v1/orders/42200 + body + ETagSafe, cacheable, repeatable
GET /v1/orders/42 with If-None-Match: "v1"304, no bodyCheap revalidation
GET /v1/orders?status=open&limit=25200 + list + cursorFiltered collection
PATCH /v1/orders/42 body {"notes":"gift wrap"}200 + updated bodyPartial update; other fields untouched
PUT /v1/orders/42 with If-Match: "v1"200, or 412Optimistic concurrency
POST /v1/orders/42/refunds201 + LocationAn action modelled as a resource
DELETE /v1/orders/42 once shipped409 + problem bodyConflict with current state
DELETE /v1/orders/99404No such resource
Any request with no token401 + WWW-AuthenticateAuthentication 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.