API design and versioning

The HTTP/APIs basics lesson covered what an API is. This is about what makes one genuinely good to use — resource-oriented URLs, using status codes and methods as they're meant to be used, and the specific problem of changing an API that other people already depend on.

Intermediate

4 min read

Resource-oriented URLs: nouns, not verbs

Bad:  GET /getUser?id=5
      POST /createUser
      POST /deleteUser?id=5

Good: GET /users/5
      POST /users
      DELETE /users/5

A RESTful API models URLs around resources (nouns — users, orders, products) and uses the HTTP method to express the action, instead of encoding the action into the URL itself. GET /users/5 (fetch), POST /users (create), PUT /users/5 (replace), DELETE /users/5 (remove) all target the same resource, /users, with the method carrying the verb — this is genuinely more predictable to a client than /getUser, /deleteUser, /removeUserAccount each inventing their own naming convention for what's fundamentally the same kind of operation on a different resource.

Using status codes and methods the way they're meant to be used

MethodURLExpected StatusNotes
GET/users/5200 OK / 404 Not FoundIdempotent — safe to retry
POST/users201 CreatedNot idempotent — creates a new resource each time
PUT/users/5200 OKIdempotent — replaces the resource
DELETE/users/5204 No ContentIdempotent — nothing to return

This connects directly to the idempotency lesson's coverage of which HTTP methods are safe to retry: GET, PUT, and DELETE are idempotent by convention (repeating them leaves the same end state), while POST typically isn't (repeating it creates a new resource each time) — a well-designed API respects this rather than fighting it, using POST only for genuine creation and reserving PUT/PATCH for updates that are safe to retry. Returning 200 for everything, with the actual outcome buried in a JSON body, forces every client to parse the response just to know whether something succeeded — exactly the problem the earlier DRF lesson's status-code discussion covered from the Django-specific side.

Versioning: the problem that shows up the moment other people depend on your API

GET /v1/users/5
GET /v2/users/5

The moment an API has real consumers — a mobile app already shipped to users' phones, a partner's integration, a frontend deployed separately from the backend — changing the API's shape (renaming a field, changing what a status code means, removing a field) breaks every client still expecting the old shape, and unlike your own codebase, you usually can't force external clients to update on your schedule. Versioning is the general answer: keep the old version running unchanged for existing clients, while new clients (or clients that have explicitly upgraded) use the new version.

The most common approach: versioning in the URL

GET /v1/orders/42    -> old response shape
GET /v2/orders/42    -> new response shape, both served by the same backend

Putting the version directly in the URL path (/v1/, /v2/) is simple, visible in logs and browser history, and easy for a client to explicitly choose. The alternative — a version specified in a request header instead of the URL — is arguably more "correct" in a REST-purist sense (the resource's identity, /orders/42, doesn't actually change between versions, only its representation does), but it's less visible and easier for a client to get wrong by omitting the header entirely. Both are real, commonly used approaches — URL versioning is simpler to reason about and far more common in practice, which is why it's the default most APIs reach for.

What actually counts as a breaking change

The general rule: adding something new is almost always safe, because existing clients simply ignore fields or endpoints they don't know about. Removing or changing something existing clients are already relying on is what actually breaks them — this is the same asymmetry the class-based views lesson's discussion of extending vs. modifying existing code touches on, just applied to a network-facing contract instead of a Python class. A genuinely careful API adds new fields freely and treats removing or repurposing an existing field as a breaking change requiring a new version.

Deprecation: the honest way to eventually remove an old version

Response headers on a v1 request:
Deprecation: true
Sunset: Sat, 31 Dec 2026 23:59:59 GMT
Link: <https://api.example.com/v2/orders/42>; rel="successor-version"

Simply deleting an old API version the moment a new one ships breaks every client that hasn't migrated yet, often without warning. A deprecation period — running the old version alongside the new one, with clear signals (response headers, documentation, direct communication to known consumers) that it will eventually be removed on a specific date — gives clients real time to migrate on their own schedule, rather than discovering the old version is gone only when their integration starts failing.

Further reading

Check your understanding

A quick comprehension check — not tracked, not graded, just for you.

1. What makes GET /users/5, POST /users, DELETE /users/5 a more RESTful design than /getUser, /createUser, /deleteUser?

2. Why does an API need a versioning strategy the moment it has real external consumers?

3. Why is adding a new optional field to an API response usually considered safe, unlike renaming an existing field?

4. Why is a deprecation period preferable to immediately deleting an old API version once a new one ships?