API Versioning & Best Practices
API Versioning & Best Practices
You have built a working CRUD REST API. Now comes the professional craft: how do you evolve that API over time without breaking existing clients? And how do you name and structure it so that it stays maintainable as it grows? This lesson covers the three mainstream versioning strategies, the naming conventions that make an API self-documenting, and a brief look at HATEOAS — the constraint that takes REST to its purest form.
Why Versioning Matters
Once clients depend on your API, any breaking change — removing a field, renaming a property, changing a status code's meaning — can break them silently or noisily. Versioning gives you a way to introduce those changes on a new version while keeping the old one alive until clients migrate. A version is a contract: everything within a version behaves consistently.
Strategy 1 — URI Path Versioning
The version number lives directly in the URL path: /api/v1/products, /api/v2/products. This is the most visible strategy and the most common in public APIs (Twitter, GitHub, Stripe all use it).
In Spring Boot you implement it with nothing more than a prefix on your @RequestMapping:
The two controllers share the same service layer; only the DTO shapes and the URL prefix differ. Keep service logic in one place — duplication lives at the controller/DTO layer only.
com.example.api.v1.controller, com.example.api.v2.controller. This keeps both versions navigable side-by-side and makes it obvious when a version is safe to delete.
Strategy 2 — Request Header Versioning
The version is passed in a custom HTTP header — commonly X-API-Version: 2 or an Accept header with a custom media type. The URL stays clean (/api/products) and a single endpoint method can branch on the header, or you can use Spring's headers attribute on @GetMapping:
Header versioning keeps URLs tidy but has a real downside: URLs are no longer independently bookmarkable or cacheable by a CDN without extra Vary configuration. It also makes versioning invisible in browser address bars and in logs unless you specifically log headers.
Strategy 3 — Media-Type (Accept Header) Versioning
This is the RESTfully purest approach: the client specifies the exact representation it wants via the Accept header using a custom vendor media type, and Spring routes to the correct handler via produces:
Accept: */*, so you must document the exact media type string, test it explicitly, and ensure your gateway does not strip custom Accept headers. Many teams reach for it and switch back to URI versioning when onboarding friction rises.
Choosing a Strategy — Trade-offs at a Glance
- URI versioning — Highly visible, trivially cacheable, easy to test in a browser. Slightly less "RESTful" because the URL should identify a resource, not a version. Best choice for public or large-scale APIs.
- Header versioning — Clean URLs, invisible to CDN caches without extra
Varyheaders, harder to test without a client that sets custom headers. Good for internal APIs with controlled clients. - Media-type versioning — Most aligned with HTTP semantics. High developer friction for consumer teams; avoid for public APIs.
For most teams, URI versioning is the pragmatic default. GitHub, Stripe, Twilio, and the majority of mature public APIs use it for exactly that reason.
REST API Naming Conventions
Good naming makes an API self-documenting. These rules are widely adopted across the industry:
- Use nouns, not verbs —
/products, not/getProducts. The HTTP method supplies the verb. - Plural collection names —
/users,/orders. A collection is a set of resources. - Lowercase, hyphen-separated words —
/product-categories, notproductCategoriesorproduct_categories. URLs are case-sensitive on some servers; lowercase removes ambiguity. - Hierarchy reflects relationships —
/users/{userId}/orders/{orderId}. Nest sub-resources under their parent, but do not go deeper than two or three levels or URLs become unwieldy. - Filter, sort and paginate via query params —
/products?category=electronics&sort=price&page=2&size=20. Keep the path clean; the path is the resource identity, query params are optional constraints. - Use standard HTTP status codes consistently —
201 Createdwith aLocationheader after a POST,204 No Contenton a successful DELETE,404 Not Foundwhen a resource does not exist (not200with an error body).
{ "status": 404, "error": "Not Found", "message": "Product 42 not found", "timestamp": "..." } — lets clients write one error-handler for your whole API instead of guessing the shape per endpoint.
A Note on HATEOAS
REST as defined by Roy Fielding includes a constraint called Hypermedia As The Engine Of Application State (HATEOAS). In a HATEOAS API, every response includes links that tell the client what it can do next — so the client does not need to hard-code URLs, it discovers them at runtime:
Spring provides the Spring HATEOAS library (starter: spring-boot-starter-hateoas) with EntityModel, CollectionModel, and WebMvcLinkBuilder to construct these link structures without manually building strings.
HATEOAS makes the API truly self-describing and decouples clients from hard-coded URL structures. In practice, most teams building private or partner-facing APIs skip it because the tooling overhead is significant and most clients do not dynamically follow links. It is most valuable in public APIs with many heterogeneous consumers. Knowing it exists — and recognising the _links pattern when you see it — is the important takeaway here.
Summary
URI path versioning (/api/v1/) is the pragmatic, industry-standard choice for most APIs. Header and media-type strategies trade URL clarity for HTTP purity at the cost of client complexity. Solid naming conventions — plural nouns, lowercase-hyphen paths, verbs from HTTP methods, query params for filtering — turn a working API into a well-designed one. HATEOAS is the REST ideal worth understanding, even if you do not implement it on day one. In the final lesson of this tutorial you will bring all of these skills together to build a complete, production-quality REST API from scratch.