What makes an API RESTful
A REST API exposes data as resources at URLs, acts on them with standard HTTP methods, and treats every request as self-contained. REST (representational state transfer) is the architectural style Roy Fielding described in his 2000 doctoral dissertation, based on how the web already worked.
Fielding defined REST as a set of constraints: client-server separation, statelessness, cacheability, a uniform interface, a layered system and optional code on demand. Many APIs called REST follow only part of the list, most often leaving out hypermedia links.
Statelessness means the server keeps no client session between requests. Each request carries its own credentials and context, so any server instance can handle it. That makes load balancing, scaling and caching simpler.
Name resources with nouns in the URL
A resource is anything clients work with through the API, such as an order, a customer or an invoice. Give each collection a plural noun and each item an ID, and let the HTTP method express the action.
Avoid verbs in paths such as /getOrder or /createOrder, which repeat what the method already says. An action that has no natural method can become a sub-resource, such as POST /orders/123/cancellation. A typical layout:
- /orders is the collection of orders.
- /orders/123 is one order.
- /customers/42/orders lists the orders that belong to customer 42.
- /orders?status=shipped&sort=-created_at filters and sorts the collection with query parameters.
Use HTTP methods for what they promise
HTTP defines what each method promises, and clients, caches and retry logic rely on those promises. A safe method only reads, so calling it changes nothing on the server. An idempotent method has the same effect on the server whether it runs once or several times.
This matters when a request times out and the client can't tell whether the server acted. Resending a PUT or DELETE is harmless, but resending a POST may create a duplicate. Many APIs accept an Idempotency-Key header on POST so the server can recognize a retry (an IETF draft, not yet a standard).
The main methods behave like this:
- GET /orders/123 reads an order. It is safe and idempotent, and responses can be cached.
- POST /orders creates an order. It is neither safe nor idempotent. Return 201 Created with a Location header that points to the new resource.
- PUT /orders/123 replaces the whole order with the request body. It is idempotent.
- PATCH /orders/123 changes only the fields you send. Whether a repeat has the same effect depends on the change: setting a field does, incrementing a counter doesn't.
- DELETE /orders/123 removes the order. It is idempotent, because a second call leaves the server in the same state even if it returns 404.
Return status codes that match the outcome
The status code is the first thing a client checks, so it should describe the result on its own. Returning 200 OK with an error message in the body forces every client to parse the body before it knows something failed.
Clients also handle errors more reliably when every endpoint returns them in one shape. RFC 9457, Problem Details for HTTP APIs, defines a JSON format for this with the media type application/problem+json. Its standard members are type (a URI naming the kind of problem), title, status, detail and instance. You can add your own members, such as a list of field errors.
Keep stack traces, SQL and internal hostnames out of error bodies. The codes most APIs need:
- 200 OK for a successful read or update, 201 Created after creating a resource, and 204 No Content when there is no body to return
- 400 Bad Request for a request the server can't parse; many APIs use 422 Unprocessable Content for well-formed input that fails validation
- 401 Unauthorized when credentials are missing or invalid, and 403 Forbidden when the caller is known but not allowed
- 404 Not Found for a missing resource, and 409 Conflict when the request clashes with current state, such as a duplicate
- 429 Too Many Requests when the client has hit a rate limit
- 500 Internal Server Error for unexpected failures, and 503 Service Unavailable for temporary overload or maintenance
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json
{
"type": "https://api.example.com/problems/validation-error",
"title": "Your request is not valid.",
"status": 422,
"detail": "2 fields failed validation.",
"errors": [
{ "pointer": "#/quantity", "detail": "must be a positive integer" },
{ "pointer": "#/currency", "detail": "must be one of USD, EUR, GBP" }
]
}Paginate collections and version only for breaking changes
Return large collections in pages, and cap the page size on the server. Offset pagination (?limit=50&offset=100) is easy to build. It gets slower at deep offsets, and it can skip or repeat items when rows change between requests.
Cursor pagination returns an opaque cursor that marks the position, and the client sends it back to get the next page. It stays stable as data changes. Either way, include the next link or cursor in the response.
Introduce a new version only when you need a breaking change, and keep the old version running while clients move. New endpoints and optional fields fit into the current version. The usual places to put the version:
- In the path, as in /v1/orders. It is easy to see, route and cache.
- In a header, either a custom one or a media type such as application/vnd.example.v2+json in Accept. URLs stay stable, but requests are harder to try from a browser.
- In a query parameter, as in /orders?version=2. It is easy to add and easy to forget, so define a default.