Module P-10·23 min read

REST constraints and resource modeling, versioning strategies, cursor vs offset pagination, OpenAPI 3.0 spec, Swagger UI — designing APIs that don't need a changelog.

Module P-10 — REST API Design and OpenAPI Documentation

What this module covers: A well-designed API is a product. Developers who consume it should be able to predict its behaviour, discover its capabilities, and upgrade to new versions without their code breaking. This module covers the REST constraints that produce predictable APIs, resource modelling, versioning strategies and their trade-offs, offset vs cursor pagination, the HTTP status codes that actually matter, and generating interactive OpenAPI 3.0 documentation from your Express routes with Swagger UI.


REST Constraints: What Makes an API RESTful

REST (Representational State Transfer) is not a standard — it is a set of architectural constraints defined by Roy Fielding. An API that follows them is predictable and easier to consume.

The six constraints, and what they mean in practice:

1. Uniform interface — a consistent way to interact with all resources. In practice: use HTTP methods semantically, use nouns not verbs in URLs, use standard status codes.

2. Stateless — each request contains all information needed to process it. No session state stored on the server between requests. Authentication via token (not session cookie) follows this constraint.

3. Client-server separation — the client and server evolve independently. Your mobile app and your API can be deployed separately.

4. Cacheable — responses must declare whether they can be cached. Use Cache-Control, ETag, and Last-Modified headers.

5. Layered system — the client doesn't know if it's talking to the actual server or a proxy/load balancer.

6. Code on demand (optional) — servers can send executable code to clients (e.g., JavaScript). Rarely used in APIs.

Most "REST APIs" only follow constraints 1–3. That's fine. What matters practically is the uniform interface.


Resource Modelling: URLs Are Nouns

URLs identify resources. HTTP methods express what to do with them.

text

Nested resources — when a resource only makes sense in the context of another:

text

Limit nesting depth — more than two levels becomes unwieldy. /users/42/posts/7/comments/3/likes is hard to read and hard to maintain. At that depth, consider a top-level resource: GET /comments/3/likes.

Actions that don't fit CRUD — some operations are not naturally resource-oriented. Use sub-resources with a verb:

text

These are acceptable when the alternative (overloading PATCH) would be unclear.


HTTP Methods and Idempotency

MethodIdempotentSafeUse for
GETRead — never modify state
HEADLike GET but no body — check existence/headers
POSTCreate, or actions with side effects
PUTFull replacement — same result if called multiple times
PATCHPartial update
DELETEDelete — second call returns 404, not an error

Idempotent: calling the operation N times produces the same result as calling it once. Idempotency enables safe retries — crucial for unreliable networks. Design your API to support this: DELETE returning 404 on a second call is correct, not an error.

PATCH vs PUT:

typescript

For most APIs, PATCH is the right choice for updates — PUT requires the client to know the full current state.


HTTP Status Codes That Matter

Use the smallest set of status codes necessary. Consistency beats completeness.

text

A note on 400 vs 422: both are used for validation errors. 400 is more common, 422 is technically more correct for semantically invalid input that is syntactically valid. Pick one and be consistent — mixing them confuses clients.


API Versioning Strategies

APIs change. Versioning lets you make breaking changes without breaking existing clients.

Strategy 1: URL versioning — most common, most visible:

text
typescript

Pros: obvious, easy to test in browser, easy to route at the load balancer. Cons: URLs are supposed to identify resources, not API versions.

Strategy 2: Header versioning — cleaner URLs:

text
typescript

Pros: clean URLs. Cons: harder to test, often stripped by proxies.

Strategy 3: Content negotiation:

Accept: application/vnd.myapi.v2+json

Used by GitHub and Stripe. Most expressive, most complex.

The pragmatic choice: URL versioning (/v1/, /v2/) for public APIs. Header versioning for internal APIs. Start with /v1/ from day one — retrofitting versioning is painful.

What counts as a breaking change:

  • Removing or renaming a field in a response
  • Changing a field's type
  • Removing an endpoint
  • Changing authentication requirements
  • Making an optional field required

What is NOT a breaking change:

  • Adding new optional fields to responses
  • Adding new optional query parameters
  • Adding new endpoints

Pagination: Offset vs Cursor

Offset pagination — simple but has problems at scale:

text
typescript

Problems:

  • Page drift: if an item is inserted while paginating, page 3 starts one record later — items get skipped or duplicated.
  • Performance: SELECT ... OFFSET 10000 forces the database to scan and discard 10,000 rows.

Cursor pagination — solves both problems, harder to implement:

text
typescript

Cursor pagination is O(1) at any depth and is immune to page drift. Use it for feeds, activity streams, and any large dataset. Offset pagination is fine for admin UIs with small datasets where users need to jump to a specific page.


Filtering, Sorting, and Field Selection

Standard query parameter conventions:

typescript
typescript

OpenAPI 3.0 Documentation

OpenAPI (formerly Swagger) is the standard for describing REST APIs. A valid OpenAPI spec generates interactive documentation, client SDKs, and test mocks.

bash

Defining the spec

typescript

Annotating routes with JSDoc

typescript

Serving Swagger UI

typescript

Visit http://localhost:3000/docs — interactive documentation where you can send real requests.


ETag Caching

ETags let clients avoid downloading unchanged resources:

typescript

Summary

  • URLs are nouns, methods are verbs. /users/42 is the resource; DELETE /users/42 is the operation. Verbs in URLs are a design smell.
  • Limit nesting to two levels. Deeper nesting is hard to maintain. Prefer top-level resources with filter params over deep nesting.
  • PATCH for partial updates, PUT for full replacement. PATCH is almost always what you want.
  • Idempotency matters for reliability. GET, DELETE, and PUT should be safely retryable. Design PATCH with idempotency in mind.
  • URL versioning (/v1/, /v2/) is the pragmatic choice. Start with /v1/ on day one.
  • Cursor pagination for large datasets and feeds — no page drift, O(1) query at any depth. Offset pagination for small admin datasets where jumping to page N matters.
  • OpenAPI 3.0 spec + Swagger UI = interactive docs your API consumers can use immediately. Generate from JSDoc comments in route files — documentation stays co-located with the code.

Next: GraphQL with Apollo Server — schema-first API design, queries, mutations, subscriptions, the N+1 problem and how DataLoader solves it, and when GraphQL is the better choice over REST.


Knowledge Check

When designing a REST API, what is the key difference between the HTTP PUT and PATCH methods?


Why is cursor-based pagination generally preferred over offset-based pagination for large datasets or real-time feeds?


Which of the following is considered a breaking change when versioning an API?

Test your knowledge with more question sets

Sign in to access a wider variety of questions and get notified when new practice sets are added to this module.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.