A good API versioning strategy comes down to three decisions. Version only when a change would break existing clients. Put the major version in the URI path unless you have a specific reason not to. Announce every retirement with a dated, machine-readable warning long before you switch anything off. Everything below explains why, with the trade-offs and a rollout plan you can copy.
I'm Hanad, a senior software engineer who builds first versions of products for founders. Most of the APIs I see in rescue projects don't fail because of the versioning scheme. They fail because nobody decided what counts as a breaking change, and the first one went out unannounced.
Key Takeaways
- Add a new version only for breaking changes. Additive changes (new optional fields, new endpoints) should ship in place.
- For public APIs, a major version in the URI path (
/v1/) is the simplest and most debuggable choice. Headers suit internal APIs where every client is code you control. - Version the major number only. Minor and patch changes belong in your changelog and OpenAPI document, not in the URL.
- Deprecation is a process: announce, add
DeprecationandSunsetheaders, measure usage, then retire. - Write the policy down before your first external client integrates. It costs an afternoon now and weeks later.
Table of Contents
- What API versioning actually solves
- When a change needs a new version
- Four ways to carry the version
- Version less: design for compatibility first
- Rules for running two versions at once
- Deprecating and retiring an old version
- A versioning policy that fits on one page
- Perspective
- Need a first version with a clean API?
- FAQ
- Sources
What API versioning actually solves
An API is a contract. Once someone else's code depends on it, you can't change it unilaterally. Zalando's API guidelines put it plainly: APIs are contracts that cannot be broken by one side's decision. Versioning is how you keep evolving your product while those clients keep working.
Google describes the goal well in AIP-185: producers can iterate quickly and safely, while consumers update to new features on their own schedule, pinning to any supported version in the meantime.
That last part matters for a small team. Every version you keep alive is a version you test, document and secure. Versioning is not free, so the best strategy is the one that makes you need it as rarely as possible.
When a change needs a new version
The rule I use: if a change would force a consumer to modify their code to keep the same functionality, it is breaking. Odown's guide states the same test.
Breaking changes typically include:
- Removing or renaming a field, endpoint or resource
- Changing a field's data type
- Making an optional parameter required, or adding a new required request field
- Changing the response format or the error format
- Changing the authentication mechanism
- Renaming URL paths or parameters
This list agrees across Zuplo's overview and HubSpot's guide.
Non-breaking changes include adding new optional response fields, adding new endpoints, adding new optional parameters, and bug fixes that preserve existing behavior (also per Zuplo).
Two caveats from experience. First, "non-breaking" depends on how your clients behave. Red Hat's engineers warn that a new optional field can still break a consumer that is tightly coupled to the spec and tolerates no variation. Second, error messages and ordering are part of the contract in practice, even if you never documented them. Say in your docs what clients may rely on and what they may not. Tell consumers to ignore unknown fields.
The three kinds of compatibility
Google's AIP-180 is a useful checklist because it separates three things teams often blur together:
| Type | Question it answers |
|---|---|
| Source compatibility | Does code written against the old client library still compile and run against the new one? |
| Wire compatibility | Do serialization and deserialization expectations still match? |
| Semantic compatibility | Does the code still receive what a reasonable developer would expect? |
Semantic compatibility is the one that bites. A field keeps its name and type but starts returning cents instead of dollars. Nothing fails to parse, and everything is wrong. Treat that as breaking.
Four ways to carry the version
There is no standard way to carry a version, as the Insper platform guide notes. These are the four common options and how they compare.
| Method | Example | Strength | Weakness |
|---|---|---|---|
| URI path | /v1/invoices | Visible, easy to route, cache and test in a browser | Version is part of the resource address, which purists say breaks REST |
| Query parameter | /invoices?version=1 | Easy to add, optional | Gets messy when mixed with resource versions |
| Custom header | API-Version: 2026-01-01 | Clean URIs | Hidden, harder to test and document |
| Media type | Accept: application/vnd.acme.v2+json | Versions at the resource level, closest to REST theory | Hardest to test, stretches the purpose of HTTP headers |
The trade-off summary for the first three and for media types follows Red Hat's reference.
URI path: the default I recommend
For public or partner-facing APIs, the path wins on practicality. You can paste a URL into a browser, read it in a log line, route it at the edge, and see at a glance which version a customer is hitting. Insper's guide calls the path the most common choice for public APIs because it is the easiest to see, route and debug.
There is a caching argument too. Without a Vary: Accept header, a shared cache that served a v1 response first can hand that body to the next client asking for v2. One practitioner write-up points out that URI versioning avoids this class of bug by construction, since two URLs are two cache keys.
Google takes the same route: AIP-185 requires a major version as the first part of the path, written as v1, not v1.0 or v1.4.2.
Headers and media types: when they fit
Header versioning suits mature APIs with stable resource structures and mostly developer-facing use, according to DOTNET.REST. You get clean URIs and flexible routing. You pay in visibility, test complexity and documentation effort. Insper adds that headers are common for internal APIs whose clients are all code.
If you go this way, never default to "latest" when the header is missing. One walkthrough warns that defaulting to latest silently breaks clients on the day you ship a new version. Default to the oldest supported version, or reject the request.
A decision rule
- Outside developers, partners or a mobile app in the wild? Use the URI path.
- Only your own services and front end, deployed together? A header is fine, or skip versioning and keep changes compatible.
- A very large surface where individual resources evolve at different speeds? Consider media types, and budget for the extra testing.
Whichever you pick, use it everywhere and describe it in your OpenAPI document. Consistency across services matters more than the choice itself.
Version less: design for compatibility first
The cheapest version is the one you never create. Zalando's guidelines strongly encourage compatible extensions and discourage versioning, because multiple versions make systems harder to understand, test, maintain, operate and release. HubSpot frames the cost as trust: consumers' products depend on yours, so unannounced big changes damage that trust.
Practical habits that reduce the need for new versions:
- Add, don't change. New field instead of repurposed field. New endpoint instead of overloaded endpoint.
- Be strict about what you send and tolerant about what you receive. Document that clients must ignore unknown fields.
- Keep enums open. Tell clients to handle values they don't recognize.
- Start with a small surface. Every endpoint you expose is one you must keep. I cover this in what to decide before building an MVP API.
- Use a new resource variant when a compatible change is impossible. Zalando recommends this, or a new endpoint with its own domain name, over running parallel versions inside one microservice.
When you do need a major version, number it simply. Use semantic versioning in your OpenAPI document: per Red Hat's guidance, major for incompatible changes (1.1.0 to 2.0.0), minor for new features without consumer impact, patch for backward-compatible fixes. Expose only the major number in the URL. The full number lives in the spec and the changelog. If you automate content or docs publishing, the same discipline applies to how you publish through an API with stable, predictable output.
Rules for running two versions at once
Once v2 exists, v1 does not vanish. The API-University summary is useful here: compatible changes ship in place, while incompatible ones become a new major version maintained next to the unchanged API. Google's AIP-185 adds that a new major version must not depend on a previous major version of the same API, and that versions should work side by side within one client application during a transition.
For a small team, that translates into a few rules:
- Share business logic, not contracts. Keep one domain layer and write thin adapters per version that map to the shared core. Never branch business rules on version numbers all over the code.
- Freeze v1. Only security fixes and critical bug fixes. New features go to the current version.
- Test the contract. Run contract tests for every supported version in CI, generated from the OpenAPI document for that version.
- Document each version separately. A reader on v1 should never have to guess which parameters still apply.
I use this layout in the TypeScript and Node projects I build: a route prefix per major version, a shared service layer, and one OpenAPI file per version. It is deliberately plain, so the next developer understands it in an hour. If you are untangling an older system, the same approach appears in how to rebuild a legacy SaaS without stopping the business.
Deprecating and retiring an old version
Deprecation is where trust is won or lost. Google's AIP-185 requires that an older version go through a reasonable, well-communicated deprecation period before shutdown. For beta functionality it recommends 180 days of deprecation before removal, and it says deprecated functionality must not graduate to a more stable channel. Those are Google's numbers for Google's products, so treat them as a reference point, not a universal rule. For paying customers who integrated into production systems, six months is a sensible floor, and a year is kinder.
A deprecation timeline you can reuse
- Decide and write it down. Name the replacement, the migration path and the retirement date. Fuchsia's API guidelines require the deprecation note to explain why, give the removal timeline and say what to use instead. Copy that structure.
- Announce through every channel. Email to account owners, a changelog entry, a banner in the dashboard and a docs page. Clear notification and a plan for sunsetting are part of Red Hat's advice for providers.
- Mark responses. Add a
Deprecationheader and aSunsetheader carrying the retirement date to every v1 response. TheSunsetheader is defined in RFC 8594, and the ZeroExp walkthrough shows a v1 response carrying both headers. - Measure usage per client. Log version and API key on every request. Contact the remaining users personally. Five emails to the five biggest holdouts beat a hundred to everyone.
- Brown out, then retire. Optionally return errors for short windows before the final date, so surprises happen on your schedule. Then return
410 Gonewith a message that points to the migration guide. - Remove the code. A retired version left in the repository is a security liability.
Fuchsia's rule that deprecation must come at an earlier stage than removal, with at least one API level between them, expresses the same idea: never go from supported to gone in a single step.
Payments and point-of-sale integrations are the clearest case, because merchants run them unattended for years. If that is your territory, a guide to POS API integration for SaaS shows why stable contracts and long notice periods matter so much there.
A versioning policy that fits on one page
Before your first outside client integrates, write this down and publish it in your docs:
- What is breaking. Use the list above, adapted to your product.
- How versions appear.
/v1/in the path, major only. - How long you support a version. For example, a minimum of 12 months after its successor is generally available.
- How you announce. Email, changelog, response headers.
- What is exempt. Beta endpoints marked as such, with explicit time-bounded previews, as Google does.
I'd add one thing for products with AI features. Model outputs shift when providers update models, and your clients feel that as a contract change. Pin model versions behind your endpoints and treat prompt or schema changes as you would any other API change. My article on shipping AI safely with guardrails covers this side. For limits and quotas, which are also part of the contract, see API rate limiting.
Perspective
Most founders ask about versioning too late or too early. Too late means a paying customer's integration breaks on a Tuesday. Too early means a v1/v2 architecture for a product with zero external users.
My view: in the first version of a product, keep the API small, keep every change additive, and put
/v1/in the path from day one. That costs nothing and gives you room later. I build on deliberately boring tools, TypeScript, Node, SQL, so the developer after me can read the code and add/v2/without a rewrite.You also own the code from the first commit, which matters here. A versioning policy is only worth something if you control the codebase that enforces it.
Hanad Kubat
Need a first version with a clean API?
If you are building a product and want the API designed properly from the start, that is exactly what I do. I build the first real, working version in two to four weeks, at a fixed price, from €12,000, with you owning the code from the first commit. Because I work alone, with no agency layer, you speak directly to the engineer writing your code. See what I build and how it's priced.
FAQ
Should I version my API from day one?
Put /v1/ in the path from the start, since it costs nothing and leaves room to grow. Don't build multi-version infrastructure until you actually need a v2. Until then, keep changes additive so you rarely need to.
Is URI versioning really not RESTful?
Purists argue the version should not be part of a resource's address, and media type versioning is closer to the original REST specification. In practice, large providers such as Google use path versions because they are visible and easy to route and debug. For most teams, practicality wins.
How long should I keep supporting an old version?
There is no universal number. Google recommends 180 days of deprecation for beta functionality, which gives a reference point. For production customers, I suggest at least six months of notice and longer when integrations are hard to update, such as embedded or unattended systems.
Do I need a new version for every change?
No. HubSpot calls versioning only in the event of a breaking change best practice, because versioning is costly for both providers and consumers. Add optional fields and new endpoints in place, and reserve a new major version for changes that force clients to alter their code.
Sources
- Google AIP-185: API Versioning
- Google AIP-180: Backwards compatibility
- Zalando RESTful API Guidelines: Compatibility
- Fuchsia Platform API evolution guidelines
- RFC 8594: The Sunset HTTP Header Field
- Insper: Versioning REST APIs
- Red Hat: How to navigate API evolution with versioning
- Red Hat: API versioning methods, a brief reference
- Odown: API Versioning, Preventing Breaking Changes in Production
- Zuplo: Optimize API Upgrades With These Versioning Techniques
- HubSpot: API Versioning, A Marketer's Guide
- API-University: API Evolution and API Versioning
- DOTNET.REST: API Versioning Strategies
- API Patterns and Practices: Versioning
- ZeroExp: Version an HTTP API Without Breaking Existing Clients
