API · Graphql

GraphQL Versioning — A Field Guide

GraphQL Versioning — A Field Guide is the work that makes the systems talk. The API is the contract between the producer and the consumer; the contract is what determines whether.

John Kihiu12 min read

REST APIs version because breaking an endpoint's response shape breaks every client calling it, and the accepted fix is to stand up /v2/orders next to /v1/orders and maintain both. GraphQL takes a different position entirely: there should not be a v2 schema. The graph evolves by addition and deprecation, and a single schema serves every client, old and new, indefinitely. This isn't a stylistic preference — it falls directly out of how a GraphQL query works: a client only ever receives the fields it explicitly asked for, so adding a field to a type is invisible to every client that doesn't request it.

Why additive changes are safe by default

In REST, adding a field to a JSON response is usually safe, but adding a required query parameter or changing a field's type is not, and there's no schema to tell you which changes are dangerous — you find out from a broken client. GraphQL's type system makes this explicit and checkable: a new field, a new type, a new enum value, or a new optional argument are all additive and non-breaking by construction. Tools like graphql-inspector or Apollo's schema checks diff your schema against the previous published version and fail CI if a change is breaking, which is a much stronger guarantee than "we think this endpoint change is backwards compatible."

GRAPHQL · SCHEMA
type Product {
  id: ID!
  name: String!
  price: Money!
  # New field — additive, safe. Old clients simply never request it.
  taxCategory: TaxCategory @deprecated(reason: "Use taxProfile instead")
  taxProfile: TaxProfile
}

The @deprecated directive as the versioning mechanism

Instead of shipping a new API version, you mark the old field with @deprecated(reason: "...") and add the replacement alongside it. The field keeps working — existing clients are not broken — while introspection tools, GraphiQL, and IDE plugins surface a warning to anyone writing new queries against it. Most production GraphQL servers (Apollo Server, GraphQL Yoga, Hot Chocolate) log or emit metrics on deprecated field usage, which tells you exactly which clients still depend on the old field and when it's actually safe to remove — rather than guessing from a changelog nobody read.

The genuinely breaking changes

A short list of changes are unavoidably breaking, and no directive papers over them: removing a field, renaming a field (which is really a removal plus an addition), changing a field from nullable to non-null when clients might legitimately receive null, narrowing an argument's accepted values, or removing a value from an enum that a client's exhaustive switch statement depends on. These require an actual migration: deprecate first, measure usage until it drops to zero, then remove in a scheduled break — communicated the same way a REST v2 cutover would be, just without a parallel URL.

Enums are the sneaky exception to "additive is safe"

Adding a new enum value is additive in the schema, but it can silently break a client whose code does an exhaustive switch over the old set of values and throws or crashes on the unrecognized one. Teams that ship a lot of enums often add a client-side default/unknown case up front specifically to absorb future additions.

Field-level evolution beats URL-level cutover

The practical upshot is that GraphQL versioning happens at the granularity of a single field, not the granularity of an entire API surface. A REST v1-to-v2 migration is an all-or-nothing event for every client. A GraphQL schema can have three fields mid-deprecation, two fields freshly added, and one type fully removed, all at the same schema version, because each client's query only touches the parts relevant to it. This is genuinely less coordination overhead — until a client is depending on a field you need gone, at which point it becomes the same negotiation a REST version bump would have required anyway.

Track usage before you remove anything

Never remove a deprecated field on a timeline — remove it on evidence. Field-level usage analytics (built into Apollo Studio, or roll your own via resolver instrumentation) tell you the actual query volume hitting a deprecated field, which is the only reliable signal that removing it won't break someone.

Wrapping up

GraphQL's answer to "how do we version this API" is largely "we don't" — additive changes ship continuously, deprecated fields stay functional until usage drops to zero, and only genuinely incompatible changes require a coordinated removal. That trades the operational simplicity of a parallel v2 endpoint for the ongoing discipline of watching field-level usage before deleting anything.

John Kihiu
Acumatica ERP Developer · Laravel Engineer

Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.