RizTech Academy logo
RizTech Academy
Building REST APIsLesson 6 of 630 min

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 Location header (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 Accept media 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:

  1. Is a resource (plural noun), acted on by the right HTTP method — no verbs in the path.
  2. Honours method semantics and idempotency (GET safe, PUT idempotent, POST not).
  3. Returns the correct status code — never 200-with-an-error.
  4. Paginates any list, with a default and maximum page size and a stable sort.
  5. Is versioned (/api/v1/...), reserving a bump for breaking changes.
  6. 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

  1. Take an action-based endpoint (/api/getParcel) and redesign it resource-style; list the full CRUD set with correct methods and statuses.
  2. Confirm your create returns 201 + Location and your delete returns 204; find any endpoint returning 200 with an error body and fix its status.
  3. Add Pageable to a list endpoint; request ?page=0&size=5&sort=destinationCity and inspect the page metadata. Set a maximum page size.
  4. Add a /api/v1 prefix; reason about which of three changes (add a field, remove a field, add an endpoint) would require a v2.
  5. For each HTTP method, state whether it is idempotent and why it matters for a client that retries.
  6. Review a Dakiya endpoint against the six-point checklist.

Official documentation

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