REST API design: status codes, versioning, pagination
You can now build endpoints; this lesson is about building them well — the design decisions that make an API pleasant, predictable and durable for the clients that consume it. REST has conventions, and following them means clients (and other developers) can guess how your API behaves. This lesson covers resource design, status codes, pagination, and versioning — the design layer over the mechanics you have learnt, illustrated with Dakiya.
Design around resources, not actions
REST's core idea: model your API as resources (nouns) that you act on with HTTP methods (verbs), rather than inventing an action for everything. Compare:
GET /api/parcels list parcels
POST /api/parcels create a parcel
GET /api/parcels/{id} get one parcel
PUT /api/parcels/{id} replace a parcel
PATCH /api/parcels/{id} partially update a parcel
DELETE /api/parcels/{id} delete a parcel
versus the anti-pattern of action-based URLs (/api/createParcel, /api/getParcel, /api/deleteParcel).
The resource style is predictable — a client that knows the pattern can guess the endpoints — and it uses
HTTP as designed: the method is the verb, the URL is the noun. Nest sub-resources sensibly
(/api/hubs/{id}/parcels for a hub's parcels), keep resource names plural nouns, and avoid verbs in
paths. This convention is what makes REST APIs feel familiar across projects.
Use HTTP methods for their semantics
Each method has meaning a client relies on, including idempotency (does repeating it change anything?):
- GET — read, never modifies (safe), and idempotent. Cacheable.
- POST — create, not idempotent (two POSTs create two resources). Returns 201 Created with a
Locationheader (Dakiya's create does exactly this — verified 201 + Location). - PUT — replace the whole resource, idempotent (same PUT twice = same result).
- PATCH — partially update, generally not idempotent.
- DELETE — remove, idempotent (deleting twice leaves it deleted), typically 204 No Content.
Honouring these matters because clients, proxies and caches assume them — a GET that modifies data, or a non-idempotent PUT, breaks expectations and causes real bugs (a retried PUT should be safe; a retried POST should not double-charge). Match the method to the semantics.
Return the right status code
The status code is the first thing a client reads — get it right (the error-handling lesson listed them):
- 200 OK — successful GET/PUT/PATCH with a body.
- 201 Created — successful POST that created a resource (with
Location). - 204 No Content — success with no body (a DELETE, or a PUT returning nothing).
- 400 / 422 — invalid input.
- 401 / 403 — not authenticated / not allowed.
- 404 — resource not found.
- 409 — conflict (duplicate, version conflict).
- 5xx — server error.
A common mistake is returning 200 with an error inside the body ("success: false") — this lies to the client and every tool that reads the status. Use the status code to say what happened; Dakiya returns 201 on create, 404 on a missing parcel, 400 on invalid input — each verified, each honest.
Pagination: never return an unbounded list
A GET /api/parcels that returns every parcel is a problem at scale — slow, huge, and unusable on a
mobile client. Paginate list endpoints from the start. Spring Data makes this nearly free with Pageable
(the data module returns to it):
@GetMapping
public Page<ParcelResponse> list(Pageable pageable) { // ?page=0&size=20&sort=destinationCity
return repository.findAll(pageable).map(ParcelResponse::from);
}
A request like ?page=0&size=20&sort=destinationCity,asc returns one page plus metadata (total elements,
total pages, whether there is a next page). The client reads the page and requests the next. Decide a
sensible default page size (20 to 50), a maximum (so a client cannot request a million at once), and a
stable sort (so pages do not overlap or skip). Retrofitting pagination after clients depend on an
unpaginated response is a breaking change — build it in now.
Versioning: let the API evolve without breaking clients
APIs change, and clients cannot all update at once. Versioning lets you evolve without breaking existing consumers. The common approaches:
- URI versioning —
/api/v1/parcels,/api/v2/parcels. Simplest and most visible; the version is right in the URL. The most widely used. - Header versioning — a custom header or an
Acceptmedia type carries the version. Cleaner URLs, less obvious, more work.
For most projects, URI versioning (/api/v1/...) is the pragmatic default — easy to route, easy to see,
easy to test. Introduce a version prefix from the start (v1), so that when a breaking change is
unavoidable you can offer v2 alongside v1 and migrate clients over time rather than breaking them all at
once. The key principle: additive changes (new optional fields, new endpoints) do not need a new version;
breaking changes (removing/renaming fields, changing types) do — so design responses to be extensible and
reserve a version bump for genuinely breaking changes.
The design checklist
Pulling it together, a well-designed REST endpoint:
- Is a resource (plural noun), acted on by the right HTTP method — no verbs in the path.
- Honours method semantics and idempotency (GET safe, PUT idempotent, POST not).
- Returns the correct status code — never 200-with-an-error.
- Paginates any list, with a default and maximum page size and a stable sort.
- Is versioned (
/api/v1/...), reserving a bump for breaking changes. - Uses DTOs and consistent errors (previous lessons) so the contract is clean and stable.
None of this is Spring-specific — it is REST done well — but Spring makes each part easy (ResponseEntity
for status, Pageable for pages, a path prefix for versions). An API built to this checklist is one clients
can rely on and you can evolve, which is the difference between an API that lasts and one that fights its
consumers.
Check your work
Resource-oriented design. Model resources (plural nouns) acted on by HTTP methods; no verbs in paths
(POST /parcels, not /createParcel) — predictable and idiomatic.
Method semantics/idempotency. GET safe+idempotent+cacheable; POST creates, not idempotent, 201+Location (verified); PUT replaces, idempotent; DELETE idempotent, 204 — clients/caches rely on these.
Status codes. The right one per outcome (201 create, 204 delete, 400/404/409, 5xx); never 200 with an error in the body. Dakiya's are verified honest.
Pagination. Paginate lists from the start with Pageable (default + max size, stable sort); retrofitting
is a breaking change.
Versioning. URI versioning (/api/v1/...) as the pragmatic default; additive changes need no bump,
breaking changes do — so design extensibly and version deliberately.
Practice
- Take an action-based endpoint (
/api/getParcel) and redesign it resource-style; list the full CRUD set with correct methods and statuses. - Confirm your create returns 201 +
Locationand your delete returns 204; find any endpoint returning 200 with an error body and fix its status. - Add
Pageableto a list endpoint; request?page=0&size=5&sort=destinationCityand inspect the page metadata. Set a maximum page size. - Add a
/api/v1prefix; reason about which of three changes (add a field, remove a field, add an endpoint) would require av2. - For each HTTP method, state whether it is idempotent and why it matters for a client that retries.
- Review a Dakiya endpoint against the six-point checklist.
Official documentation
- Spring — REST API design and pagination (Spring Data
Pageable) — Pagination support. - MDN — HTTP request methods — Semantics and idempotency.
- RFC 9457 — Problem Details — The standard error format for consistent APIs.
Next: JPA, Hibernate and entities.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship