when a single schema has to grow across years
GraphQL doesn't put version numbers in the URL, and that's intentional, not an accident. Running a GraphQL API stably over years requires a different discipline: additive schema evolution, clearly defined deprecation cycles, and automated checks that catch breaking changes before they ever get deployed.
Table of Contents
- 1. Why GraphQL doesn't need version numbers in the URL
- 2. What actually counts as a breaking change
- 3. Additive evolution: new fields instead of new endpoints
- 4. Using and communicating @deprecated correctly
- 5. Nullable fields as a safe extension tool
- 6. Schema registry: catching breaking changes before deploy
- 7. Actually measuring usage of deprecated fields
- 8. Governance: who's allowed to change the schema, and how
- 9. Schema evolution compared to REST versioning
- 10. Summary
- 11. FAQ
1. Why GraphQL doesn't need version numbers in the URL
With REST APIs, /v1/products followed by /v2/products is an established pattern once the data structure fundamentally changes. GraphQL schema versioning works deliberately differently: there's exactly one schema that evolves over time, without clients needing to switch to a new version. The reason lies in the request model itself, a GraphQL client requests exactly the fields it needs, new fields in the schema have zero impact on existing clients that don't request them.
This property fundamentally makes GraphQL schema versioning an additive discipline rather than version-number management. Instead of maintaining parallel API versions, which multiplies maintenance effort, a single schema grows continuously, old fields stay in place until clients have actively migrated to new alternatives. The price is discipline: every schema change has to be deliberately assessed for whether it's additive or breaks existing clients.
The most common mistake with GraphQL schema versioning is assuming GraphQL makes breaking changes automatically impossible. That's not true, removing a field, changing its type, or making an optional argument required breaks existing clients just as much as with REST. The difference is only that GraphQL provides the tools to avoid such changes or handle them through a controlled deprecation cycle, instead of forcing them through immediately.
These tools only deliver value if a team actually applies them consistently. A schema that could formally grow additively but in practice regularly suffers unannounced breaking changes because deadlines create pressure loses the same trust with client teams as a poorly maintained REST API. GraphQL schema versioning is therefore ultimately less a technical than an organizational task, the following sections cover both sides equally.
2. What actually counts as a breaking change
For clean GraphQL schema versioning, a team needs a clear, shared definition of what counts as a breaking change. As a general rule: any change that invalidates an existing, valid query or alters its result in a way the client doesn't expect is a breaking change. That includes removing a field or type, changing a field's type, say from String to Int, adding a required argument to an existing field, and changing a nullable field to non-nullable when the server can actually still occasionally return null.
Not every change that looks risky at first glance is actually a breaking change. Marking a field as @deprecated doesn't break any existing queries, it still returns the same data. Adding a new optional argument to a field is additive, as long as a sensible default exists. This distinction is the core of any solid strategy for GraphQL schema versioning, additive changes can be deployed at any time, removals or alterations need a deprecation process.
A gray zone is formed by changes to description text and interface implementations. A pure text change to description breaks no query, but can confuse tooling that processes descriptions automatically. A type that suddenly implements an additional interface is usually additive, a type that stops implementing an existing interface, on the other hand, is almost always a breaking change for any client accessing it via fragment spreads on that interface.
3. Additive evolution: new fields instead of new endpoints
The central building block of GraphQL schema versioning is additive evolution: instead of changing a field, a new field with the desired behavior is added while the old field stays in place. If the format of a price field, for example, needs to change from a plain float to a structured object with currency and amount, no conflict arises when both variants exist in parallel until every client has migrated to the new structure.
This strategy costs more fields in the schema up front compared to a hard migration, but pays off in stability. Existing mobile apps that might not be updated immediately keep working unchanged, while new clients already use the improved structure. Only after all known clients have migrated is the old field actually removed, never before.
# BEFORE: a plain float price field, ambiguous currency assumption
type Product {
id: ID!
price: Float!
}
# ADDITIVE EVOLUTION: new structured field added, old field kept working
type Money {
amount: Float!
currency: String!
}
type Product {
id: ID!
price: Float! @deprecated(reason: "Use priceDetails instead, ambiguous currency. Removal planned for 2027-Q1.")
priceDetails: Money!
}
# Existing clients querying `price` keep working unchanged,
# new clients migrate to `priceDetails` at their own pace
4. Using and communicating @deprecated correctly
The @deprecated directive is the central tool for controlled GraphQL schema versioning. It marks a field as outdated without removing it, and delivers a developer-readable reason via the reason argument, visible in GraphiQL, Apollo Studio, and any introspection-based tool. A good deprecation reason states three things: why the field is deprecated, which alternative to use, and by when the field is expected to be removed.
What matters for GraphQL schema versioning is that @deprecated alone isn't enough to actually get clients to migrate. Without active communication, such as changelog entries, team notifications, or automated warnings in CI pipelines that use deprecated fields, outdated fields often stay in use for years because nobody prioritizes the migration. A fixed process for actually removing deprecated fields after a defined period prevents a schema from growing indefinitely.
type Query {
# Old, ambiguous filter argument, deprecated with a clear migration path
products(status: String @deprecated(reason: "Use statusFilter (enum) instead. Removed in v2027.1.")): [Product!]!
# New, type-safe replacement using an enum instead of a free-form string
productsV2(statusFilter: ProductStatus): [Product!]!
}
enum ProductStatus {
DRAFT
ACTIVE
ARCHIVED
}
5. Nullable fields as a safe extension tool
An often overlooked tool in GraphQL schema versioning is the deliberate use of nullable fields. A new field should, in the vast majority of cases, be introduced as nullable, even if it could be populated for every existing record at launch time. The reason is future-proofing, a field that later, for business reasons, can no longer be populated for some records otherwise has to be changed from non-nullable to nullable after the fact, which is itself a breaking change.
Conversely, an existing nullable field should only be changed to non-nullable when it's absolutely certain the server will never again return null for that field, for no existing or future record. In practice, this guarantee is surprisingly hard to give, which is why experienced GraphQL teams tend to stay conservative with nullability when in doubt for GraphQL schema versioning, even if that means clients need to write extra null checks.
The same principle applies to field arguments and enum values. A new optional argument with a sensible default is additive, whereas a new enum value can surprise existing clients that wrote a switch statement over every known value and silently mishandle the new one. For GraphQL schema versioning, it therefore pays off to add an explicit note in enum descriptions from the start that clients should react defensively to unknown values, rather than relying on a fixed, closed list.
6. Schema registry: catching breaking changes before deploy
Manual reviews aren't enough to reliably catch every breaking change as teams grow. A schema registry like GraphQL Hive or Apollo Studio stores historical schema versions and automatically compares every new schema against the version currently used in production. This diff check automatically detects removed fields, changed types, and other incompatible changes before they ever reach production.
For GraphQL schema versioning, integrating such a check into the CI pipeline is the single most important protection mechanism. A pull request with a breaking change gets automatically blocked, or at least clearly flagged, before a reviewer even has to walk through the change manually. Advanced registries go further and link the schema diff with actual usage telemetry, a breaking change on a field no client has used in the past ninety days is a lower risk than the same change on a heavily used field.
# GraphQL Inspector: check schema changes against the current baseline
# in a CI pipeline, before merging
npx graphql-inspector diff \
schema-baseline.graphql \
schema-candidate.graphql \
--fail-on-breaking
# Output flags every removed field, changed type, or newly required argument
# Exit code 1 blocks the pipeline on any breaking change
7. Actually measuring usage of deprecated fields
Safely removing a deprecated field assumes it's actually known whether, and how often, it's still used. Without usage telemetry, every decision to remove a field remains a guess, one that in the worst case breaks a production client the API team didn't even know existed. For GraphQL schema versioning, field-level usage tracking is therefore not a nice-to-have, it's a prerequisite for safe removal.
Tools like Apollo Studio or custom resolver middleware can log, per field, which client name, which client version, and how often a given field is requested. Only once that metric stays consistently at zero over a defined period, say ninety days, does actually removing a deprecated field become a calculated move instead of a risky one.
An additional safety mechanism is an explicit warning inside the response itself, such as an extensions field in the GraphQL response listing which requested fields are deprecated. Some client libraries can automatically surface such warnings in the developer console, which significantly raises the visibility of deprecations without developers having to actively check the schema.
8. Governance: who's allowed to change the schema, and how
As team size grows, GraphQL schema versioning also becomes an organizational question. Without clear rules, schema sprawl, inconsistent naming conventions, and uncoordinated breaking changes from different teams working on different parts of the same schema easily emerge. A schema governance process defines who's allowed to review change proposals, which naming conventions apply, and how conflicting requirements between teams get resolved.
In federated architectures with multiple teams each contributing parts of an overarching schema, a central schema registry team often makes sense, one that enforces naming conventions and coordinates breaking-change checks across team boundaries. For smaller teams, a simple, documented review process with a mandatory schema diff check before every merge is often enough, the most important point is that the rules are clear and visibly documented for everyone involved.
A workable governance document for GraphQL schema versioning answers at least four questions: who approves a new breaking change once its deprecation cycle has completed, what minimum interval applies between announcement and actual removal, how are external partner teams or third-party clients informed who may not be visible in the same version control system, and where is the current deprecation status documented for everyone to see. Without written answers to these four questions, governance usually stays a matter of individual team members' judgment, which quickly turns into inconsistent practice once people change roles.
9. Schema evolution compared to REST versioning
The fundamental difference between GraphQL schema versioning and classic REST API versioning shows up most clearly in the maintenance structure.
| Aspect | REST versioning | GraphQL schema evolution |
|---|---|---|
| Parallel versions | Multiple complete API versions | One schema, extended additively |
| Old clients | Stay on the old version | Keep using old fields |
| Maintenance effort | Grows with every version | Constant, one code path |
| Breaking change detection | Manual, often implicit | Automated via schema diff |
| Removing old fields | Version sunset with a hard date | Driven by usage telemetry |
In practice, GraphQL schema versioning leads to less parallel maintenance effort, but demands more discipline on every single schema change, because there's no hard version boundary that technically forces mistakes into visibility.
Teams moving from a versioned REST API to GraphQL often underestimate this discipline gap at first. The apparent advantage of no longer maintaining version numbers quickly flips into a disadvantage when schema diff checks and deprecation processes are missing, because then every change potentially becomes a breaking change unnoticed, instead of vanishing in a controlled way behind a new version number.
Mironsoft
GraphQL architecture, API governance and schema design
Want your schema to stay stable for years, without breaking changes?
We set up schema diff checks in your CI pipeline, define deprecation workflows, and build usage telemetry so field removals are grounded in evidence rather than risk.
Schema diff CI
Automated breaking-change detection before every merge
Deprecation workflow
Clear processes for announcement, deadline and removal of outdated fields
Governance
Naming conventions and review processes for growing teams
10. Summary
GraphQL schema versioning works fundamentally differently from REST versioning with version numbers in the URL. Instead of parallel API versions, a single schema grows additively, new fields complement existing ones without replacing them. The @deprecated directive marks outdated fields with a clear reason and timeline, while nullable fields serve as the safe default tool for new extensions.
Automated schema diff checks in the CI pipeline catch breaking changes before they're deployed, and usage telemetry turns removing outdated fields into a grounded decision rather than a risky one. As teams grow, GraphQL schema versioning additionally needs clear governance rules, so additive evolution doesn't tip over into uncoordinated schema sprawl.
GraphQL Schema Versioning — The essentials at a glance
Additive evolution
New fields instead of changed fields, old field stays until migration is fully complete.
@deprecated
Clear reason, alternative and removal date, combined with active communication.
Schema registry
Automated diff check in the CI pipeline blocks breaking changes before deploy.
Usage telemetry
Field-level usage tracking makes removals calculated instead of risky.