API Versioning Strategies: URI, Header, and GraphQL Approaches Compared
API Versioning Strategies compare URI paths, headers, and GraphQL schemas so you can ship changes without breaking clients that you still support today.
API Versioning Strategies exist because clients do not upgrade when you do. When you change a response and an old app still parses it, the break is yours. You may not even see the error until a store review or a partner ticket. Still, adding a version to every URL is not a full plan.
A version is a promise about what you will not change. If the promise is fuzzy, people pin forever or they crash. Because of that, you should name what counts as a break before you ship the second client. Also, a version that you cannot retire will split your code path until the oldest caller dies.
What versioning is and why it fails
Versioning lets more than one contract live at the same time. Google API versioning guidance separates a major version, which may break, from additive changes inside it. When you only add fields, old clients can ignore them. Then a rename, a type change, or a stricter check is a new major version, or it is an outage.
AIP-185 says major versions are for breaking changes, and they should be rare. If every sprint gets a new major, clients stop trusting the number. Therefore most weeks you should extend, not fork. Also, a beta that people wired into production is a version, even if you called it temporary.
Production fails when two meanings hide under one version. A common mistake I have seen is a field that stays named status while the enum gains a value old code treats as success. Then retries and refunds go wrong with no deploy on the client. After that, you cannot tell who is safe from the version string alone.
The other failure is never removing the old path. If v1 still holds half the traffic years later, every fix lands twice. Still, you cannot delete it while a device in a drawer calls it. So a version strategy includes a sunset, a measure of callers, and an owner.
How the three approaches work
URI versioning puts the major version in the path. First, the client calls a path that starts with the version. Next, your router sends it to a handler set.
Finally, logs and caches key off a visible string. While that is easy to teach, it tempts people to fork the whole API for a tiny break. Because the path changed, every doc and every example must change too.
Header versioning keeps one path and puts the version in a header or in the Accept media type. Then caches and bookmarks stay put, and a default version serves old callers. If the default changes under them, you broke the promise. Also, a missing header must mean something stable, or curl users will hit the newest shape by accident.
GraphQL and additive schemas
GraphQL usually keeps one endpoint and evolves the schema. You add fields and types, and you mark old fields deprecated. When clients ask only for the fields they need, an add does not disturb them.
If you remove a field that a stored query still selects, that query fails. Therefore deprecation needs usage data from real operations, not a guess.
A breaking GraphQL change is still a break. Renaming a field, changing a type, or making a nullable field required will hurt someone. Also, a single graph shared by many teams becomes a coordination problem. So own the schema, and review removals like you review a migration.
What a break is
A break is any change a correct old client cannot survive. Removing a field, changing a JSON type, or tightening validation counts. Also, changing the meaning of an existing value counts, even when the shape looks the same. If you only add an optional field and keep every old behavior, you can stay on the same major version.
Error codes are part of the contract. When a client branches on a status, a new code can skip the branch and look like success. Then document the codes you promise, and add new ones only as extensions old code can treat as unknown. Still, do not reuse a code for a new meaning.
Trade-offs among URI, header, and GraphQL
Use a URI version when you want the contract obvious in logs, docs, and sample calls. Use a header when you want stable URLs and you can set a default that never silently changes. Also, use GraphQL when clients already pick fields and you can enforce deprecation with real usage. If you cannot see who calls you, you cannot retire anything safely.
| First choice. | When it fits. | If it breaks. |
|---|---|---|
| URI major version. | Use it when humans must see the contract in the path. | Teams fork the whole surface for a small break. |
| Header or media type. | Use it when URLs must stay stable for caches and links. | A shifting default version surprises old clients. |
| GraphQL schema evolution. | Use it when clients select fields and you can track operations. | A removed field fails stored queries that still select it. |
| Date or snapshot header. | Use it when you want many small compatible revisions. | You can accumulate more snapshots than you can test. |
Library versions and API versions are related and not the same. Semantic versioning tells a programmer whether a generated client may break at build time. An API version tells a running process whether a wire call is still valid. Also, read gRPC and REST when you choose where the version lives, because protobuf packages and URL segments teach different habits.
RFC 8594 defines the Sunset header, which is how an HTTP API can name the date an old resource goes away. If you sunset without a metric, you will delete a version that still pays you. Then the rollback is a restore plus a long cache wait. HTTP/2 and HTTP/3 do not remove this problem, since both carry the same headers and paths.
Sometimes you run two versions on different fleets. That is closer to load balancer failover than to a rewrite inside one process. When a bad v2 rolls out, you can shift weight back to v1 if the data model still matches. Also, do not share a write path that assumes only one version, or the rollback will corrupt rows.
Pitfalls and failure modes
Compatible changes still hurt when clients are strict. However, a parser that rejects unknown fields will crash on an add. You should document that unknown fields are ignored, and you should test an old client against a new response. When a mobile app was generated from an old schema and cannot be updated the same day, that test is the release gate.
Caches key on the URL. If the version is only in a header, a shared cache may store one variant and serve it to everyone. Then a v2 response lands on a v1 client.
Also, Vary must include the header you use, or the bug looks random. So test through the real cache, not only against the origin.
GraphQL fails when deprecation is a comment nobody reads. If you do not log field use, you will remove a field that a rare job still needs. Still, leaving every field forever makes the schema a junk drawer. Because of that, set a date, warn in the schema, and block removal until the metric is zero or the exception is signed off.
- First, write the list of changes that count as breaking for this API.
- Next, put the major version where logs, caches, and clients will actually send it.
- Then add one optional field and prove an old client still works.
- After that, count callers per version every week, including background jobs.
- Finally, set a sunset date and keep the old path until that count is an accepted risk.
In my experience, teams version the URL and then break behavior inside v1 anyway. While the path says v1, the meaning moved. So the version string is a lie, and clients cannot pin safety. Also, do not ship a breaking fix under the old version because a rollback feels hard.
A version header you can copy
The example shows a stable path, an explicit version header, and a Sunset hint on the old major. It is a pattern, not a mandate to copy the media type string. Also, the old call still works until the date you published. If you change the default, do it in a new major, not by surprise.
GET /orders/123 HTTP/1.1
Accept: application/vnd.shop.v2+json
API-Version: 2
HTTP/1.1 200 OK
Sunset: Sat, 01 Aug 2026 00:00:00 GMT
Deprecation: true
For a URI style, the same order would live under a version prefix, and the header can stay off. Because both can be correct, pick one and use it on every route. Then a gateway can reject a mix, such as a v1 path with a v2 header. Also, log the version you served, not only the version the client asked for, when a default filled the gap.
GraphQL can expose the same honesty with deprecation reasons that include a replacement field and a date. When a query hits a deprecated field, log the operation name. If you have no operation name, require one before you promise a sunset. Still, keep removal behind a review that looks at that log.
Performance, scale, and cost
Two full stacks cost more than one stack with additive changes. In an illustrative production range, a forked major version can double test time and the chance you patch only one side. If the versions share a database, a write from v2 can still break a v1 read. Therefore keep the data model compatible for as long as both majors live.
Header versions add a cache key. A high cardinality version header, such as a unique build id per deploy, will shatter the cache. Meanwhile, a single major digit stays cache friendly. So version majors, not git shas, on the public edge.
GraphQL cost is complexity and query cost, not header parsing. One endpoint hides a wide schema, and a heavy query can dominate a host. Also, persisted queries help you know what to support. When you allow arbitrary queries from partners, versioning gets harder because you cannot see the next selection set.
The human cost is the long tail of old clients. A sunset you do not staff becomes a forever branch. After you publish a date, assign an owner and a weekly caller report. If the tail is a partner contract, the version strategy is also a sales promise, so write it down.
Key Takeaways
- Also define a breaking change before you publish the first client.
- When you only add optional fields, keep the same major version.
- Because URI versions are obvious, they also tempt needless forks.
- If the version sits in a header, vary caches on that header.
- Still track GraphQL field use before you remove anything you deprecated.
- Therefore pair every old version with a sunset date and a caller count.
FAQ
Should every change get a new URI version?
No, because most changes can be additive and old clients can ignore new fields. A new major URI is for a break you cannot avoid. Also, too many majors train people to ignore the number. Then you pay for forks that did not buy safety.
When is a header version a bad fit?
It is a bad fit when caches, CDNs, or browsers will strip or ignore the header. If you cannot set Vary correctly, you will serve the wrong body. Also, clients that cannot set headers easily, such as some webhooks, are happier with a path. After you see a stripped header in production, move the version to the URI.
Does GraphQL remove the need for versions?
It removes many URL forks, since clients choose fields. It does not remove breaks, deprecation, or the need to count users of an old field. However, a careful additive schema can go a long time without a new endpoint. If you must break a type, introduce a new field and migrate, then remove the old one later.
How do I retire v1 safely?
First, count every caller, including jobs and partners, for several weeks. Next, send Sunset and document the date in the changelog. Then move the last callers or accept the residual risk in writing. Finally, delete the route and watch errors so a hidden client is a page, not a silent failure.
Write a one page rule for what breaks your API, and tag one live route with the version style you will keep. Add a caller metric and a sunset header or changelog date for the oldest major. After that, ship only additive changes until you truly need a new major. If you cannot name the callers, do not delete the old path this quarter.
Last updated on 16 September 2026.
[…] clients feel schema changes when response fields move. Coordinate with API versioning strategies so you do not rename a JSON field in the same hour you rewrite the column. Keep the external shape […]
[…] APIs and library APIs share this pain. API versioning covers URI and header majors for running clients. Semantic versioning covers the package those […]