REST stands for Representational State Transfer, an architectural style defined by Roy Fielding in his 2000 dissertation. Today it is the dominant pattern for web APIs, even though most APIs that call themselves "RESTful" only partially follow the constraints. Understanding what REST actually is — and what distinguishes good REST design from bad — is essential for every backend developer in 2026.
What changed in 2026
- REST remains dominant for public and partner APIs. Despite GraphQL and gRPC maturity, REST is still the default choice for public-facing APIs because it is simple to consume without specialized client tooling.
- OpenAPI 4.0 shipped. The spec for describing REST APIs standardized overlays and webhooks. All major API gateways (Kong, Apigee, AWS API Gateway) auto-generate docs and validation from OpenAPI.
- HTTP/3 (QUIC) is widely deployed. REST over HTTP/3 reduces head-of-line blocking. The REST semantics are identical; the transport is faster.
- AI clients consume REST APIs. LLM tool-calling integrations call REST APIs directly; a clean, well-documented API is now also an AI integration surface.
The six REST constraints
| Constraint |
What it means in practice |
| Client-server |
UI and backend are separate; they evolve independently |
| Stateless |
Every request carries all needed context; no server-side session |
| Cacheable |
Responses declare whether they can be cached (Cache-Control) |
| Uniform interface |
Resources, HTTP verbs, status codes, and hypermedia used consistently |
| Layered system |
Client does not know if it is talking to origin server or a proxy |
| Code on demand (optional) |
Server can send executable code (e.g., JavaScript); rarely used |
Most real-world APIs satisfy the first four. "Stateless" is the one most commonly violated.
HTTP verbs and when to use them
GET /users → list users (safe, idempotent)
GET /users/42 → get user 42 (safe, idempotent)
POST /users → create a user (not idempotent)
PUT /users/42 → replace user 42 entirely (idempotent)
PATCH /users/42 → update specific fields of user 42 (idempotent)
DELETE /users/42 → delete user 42 (idempotent)
Safe means the request has no side effects (read-only).
Idempotent means making the same request multiple times has the same effect as making it once.
Use PUT when you replace the whole resource. Use PATCH when you update a subset of fields (send only the changed fields). Do not use POST for everything — it makes your API unpredictable.
HTTP status codes — the key ones
| Code |
When to use |
| 200 OK |
Successful GET, PUT, PATCH |
| 201 Created |
Successful POST that created a resource |
| 204 No Content |
Successful DELETE (no body) |
| 400 Bad Request |
Invalid input from the client |
| 401 Unauthorized |
Missing or invalid authentication |
| 403 Forbidden |
Authenticated but not authorized |
| 404 Not Found |
Resource does not exist |
| 409 Conflict |
State conflict (e.g., duplicate email) |
| 422 Unprocessable Entity |
Semantically invalid input (passes format check, fails business logic) |
| 429 Too Many Requests |
Rate limit exceeded |
| 500 Internal Server Error |
Bug or unexpected server failure |
The most common mistake is returning 200 with {"success": false} for errors. Use the correct status code — clients and load balancers depend on it.
A minimal REST endpoint example
# FastAPI — a clean RESTful user resource
from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel
app = FastAPI()
class UserCreate(BaseModel):
name: str
email: str
class UserResponse(BaseModel):
id: int
name: str
email: str
@app.post("/v1/users", status_code=status.HTTP_201_CREATED, response_model=UserResponse)
async def create_user(body: UserCreate, db: AsyncSession = Depends(get_db)):
existing = await db.scalar(select(User).where(User.email == body.email))
if existing:
raise HTTPException(status_code=409, detail="Email already registered")
user = User(name=body.name, email=body.email)
db.add(user)
await db.commit()
await db.refresh(user)
return user
@app.get("/v1/users/{user_id}", response_model=UserResponse)
async def get_user(user_id: int, db: AsyncSession = Depends(get_db)):
user = await db.get(User, user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
URL design patterns
# Good — resource-oriented
GET /v1/orders
GET /v1/orders/99
GET /v1/orders/99/items (nested resource)
POST /v1/orders/99/cancellation (noun for an action that has state)
# Bad — action-oriented (RPC-style leaking into REST)
POST /api/createOrder
POST /api/cancelOrder?id=99
GET /api/getOrderItems?order_id=99
Nest resources when the child only exists in the context of the parent (order items belong to an order). Keep nesting to one level where possible — deep nesting becomes awkward.
Statelessness in practice
# Stateful (wrong for REST)
POST /login → server stores session in memory, returns session ID in cookie
# Stateless (correct)
POST /auth/token → server returns JWT, client sends it as
Authorization: Bearer <token> on every request
The server validates the JWT on each request using the signing key — no session lookup, no shared server memory. This scales horizontally: any server instance can handle any request.
Common mistakes
Ignoring status codes. Returning 200 for everything and encoding success/failure in the body forces every client to parse the body before knowing if the request worked. Middleware, monitoring, and retry logic all depend on status codes.
Plural inconsistency. Use plural nouns consistently: /users, /orders, /products — not a mix of /user and /orders.
Exposing database IDs as sequential integers in public APIs. Use UUIDs or opaque identifiers to prevent enumeration attacks (GET /users/1, /users/2, ...).
No versioning. Every API you do not version from day one will eventually break clients when you need to change a field type or remove a field. Add /v1/ from the start.
What to skip
- HATEOAS for most applications — the hypermedia constraint (embedding links to related resources in responses) is theoretically correct but rarely implemented or consumed in practice. Skip it unless building a public API for unknown client types.
- Custom HTTP methods —
LINK, UNLINK, PURGE exist but are non-standard. Model unusual actions as resources (POST /cancellations, POST /password-resets).
- Overly deep nesting —
/v1/companies/1/teams/3/members/7/tasks/42 is hard to use and document. Flatten where possible.
FAQ
Is REST better than GraphQL?
They solve different problems. REST is simpler to consume and cache; GraphQL is better for complex, multi-entity queries from a single endpoint. For most backend APIs, REST is the right default. See the comparison guide below.
What is HATEOAS and do I need it?
HATEOAS (Hypermedia As The Engine Of Application State) means embedding navigation links in responses. It is the "Level 3" of Richardson Maturity Model. Most production APIs stop at Level 2 (resources + HTTP verbs); Level 3 adds complexity without commensurate benefit for typical use cases.
How do I handle bulk operations in REST?
POST to a collection resource with an array body, or introduce a batch endpoint: POST /v1/users/batch. Return an array of results with per-item status codes if operations are independent.
How do I document a REST API in 2026?
Use OpenAPI 4.0. Write the spec first (or generate it from code annotations), then use Swagger UI, Redoc, or Scalar to publish it. See the documentation guide below.
Where to go next