REST API Design
REST API design turns resources and HTTP behavior into a predictable contract for clients. A URL should be designed together with resource identity, status codes, error shape, caching, and evolution policy rather than treated as only a controller path.
Quick Decision
| Need | Starting decision | Watch out |
|---|---|---|
| Resource collection | Plural URI (/users) | Avoid verb-based URIs |
| Single-resource operation | HTTP method plus resource ID | Document idempotency behavior |
| Large list | Cursor or keyset pagination | Keep the ordering key stable |
| Complex search | Query parameters or a separate search endpoint | Do not accept free-form SQL |
| Partial update | PATCH with explicit merge semantics | Distinguish null from field removal |
Production Checklist
- Problem: Are resources, sub-resources, and aggregate boundaries clear?
- Solution: Are method, status code, error body, pagination, and filter contracts defined?
- Trade-off: REST provides simplicity and HTTP tooling; complex screens may need a BFF or GraphQL.
- Failure mode: Validation, authorization, conflict, timeout, and rate-limit responses must be distinct.
- Measurement: Track p95 latency, payload size, page depth, 4xx/5xx rates, and cache hits.
- Security/cost: Restrict filter fields with an allowlist; protect large responses and expensive queries with quotas.
Resource Modeling
- Use
/ordersfor a collection and/orders/{orderId}for one resource. - Use
/orders/{id}/itemswhen the relationship is part of the resource lifecycle; use/items?orderId={id}when items are independently queried. - Do not put verbs such as
create,get, ordeletein URIs; use an explicit action endpoint only when the domain command cannot be represented as a resource. - Standardize ID formats and time representations. Carry resource metadata and contract version when it is useful to clients.
HTTP Methods and Status Codes
| Operation | Method | Success | Important error |
|---|---|---|---|
| List | GET /orders | 200 OK | 400, 401, 403 |
| Single resource | GET /orders/{id} | 200 OK | 404 Not Found |
| Create | POST /orders | 201 Created + Location | 409, 422 |
| Full update | PUT /orders/{id} | 200 or 204 | 404, 409 |
| Partial update | PATCH /orders/{id} | 200 or 204 | 400, 409 |
| Delete | DELETE /orders/{id} | 204 No Content | 404, 409 |
202 Accepted means that work was queued; 429 Too Many Requests means the client should try later; 503 Service Unavailable represents temporary capacity or dependency trouble. Use one error shape containing status, stable error code, message, field errors, request/correlation ID, and retry information when applicable.
Pagination, Filtering, and Sorting
Pagination choices
| Model | Good for | Cost |
|---|---|---|
Offset (page, size) | Small and mostly stable lists | Slow deep pages and shifts during writes |
| Cursor | Changing feeds and infinite scroll | Cursor must be opaque and have a lifetime |
| Keyset | Large tables with indexed ordering | Clients cannot page by every field |
Define stable sort, limit, nextCursor, or hasMore semantics for every list. Cap limit; document the default ordering and tie-breaker (createdAt, id).
Filtering and sorting
Use allowlisted query parameters such as status=active, createdAfter=..., and sort=-createdAt,name. Do not expose arbitrary field names or directions. Measure query plans, index requirements, and timeout budgets for filter combinations. If a search language is required, define a constrained search contract instead of passing SQL or ORM expressions to the API.
Bulk Operations and HATEOAS
Bulk endpoints must define per-item results, error codes, and idempotency behavior. Use one transaction only when all items share a safe consistency boundary; return 202 and a job-status resource for long bulk work.
HATEOAS means more than adding URLs: it allows a client to discover valid next actions.
{
"id": "ord_123",
"status": "pending",
"_links": {
"self": { "href": "/api/orders/ord_123" },
"cancel": { "href": "/api/orders/ord_123/cancellation", "method": "POST" }
}
}Link relation names must match authorization and current state. A link never replaces a server-side security check.
OpenAPI and API Performance
An OpenAPI contract defines paths, parameters, request/response schemas, status codes, security schemes, and examples. SpringDoc-generated documentation, hand-written descriptions, and generated clients should come from the same contract. See API Contracts and Tooling for the workflow.
Keep payloads small; use field selection, compression, ETags/conditional requests, and response caching where appropriate. N+1 queries, expensive sorting, and unlimited nested expansion are backend bottlenecks that are invisible at the HTTP layer.
Outcome
A good REST design makes successful and failed flows repeatable for the same client. For every endpoint, document an example request/response, error contract, auth scope, retry/idempotency behavior, and pagination rules.
