GraphQL path · At scale · Topic 11 of 12

Evolving a schema without versions

Not started

Most GraphQL APIs are never versioned. Because each client names the exact fields it uses, the server can add types and fields at any time without affecting anyone, and the schema grows continuously instead of jumping from v1 to v2. Removing or changing things is where care is needed. The built-in @deprecated directive marks what is going away, introspection and tooling show it to client developers, and the server's knowledge of which fields clients actually request tells you when removal is safe. Nothing stops you from versioning a GraphQL API like any other, but the language is designed so you rarely have to.

Why it matters: GraphQL APIs usually have no /v2. That works only if every change is checked against what clients really send; otherwise "versionless" just means breaking people without warning.

  1. 1Adding is safe

    Clients get only the fields they ask for, so new types, new fields and new optional arguments break nobody.

  2. 2Deprecate, then remove

    Mark the old field `@deprecated(reason: ...)`, point to its replacement, watch usage fall to zero, then delete it.

  3. 3Know what clients send

    Field-level usage from logged operations, or a registry of client documents, tells you whether a change is safe.

  4. 4Nullability is part of the contract

    Making a field nullable can break clients that assumed a value; making an argument required breaks every caller that omits it.

1

How it works

The explanation, with examples you can run.

Safe and breaking changes

Usually safeBreaking
Add a type, field, or enum value*Remove or rename a field, type or enum value
Add an optional argument or input fieldAdd a required argument or input field
Make an argument optionalMake an argument required, or change its type
Make an output field non-nullMake an output field nullable
Deprecate anythingChange what a field means while keeping its name

*A new enum value can surprise clients that switch exhaustively over an output enum; give them a default branch. The direction of nullability flips between inputs and outputs: a non-null output promises more (safe), a non-null input demands more (breaking).

Deprecation

type User {  id: ID!  name: String!  fullName: String @deprecated(reason: "Use `name`. Removed after 2027-01-31.")} type Query {  users(first: Int, limit: Int @deprecated(reason: "Use `first`.")): UserConnection!} enum Status {  ACTIVE  DISABLED  BANNED @deprecated(reason: "Use DISABLED with a `reason` field.")}

The October 2021 edition of the spec allowed @deprecated on fields and enum values; the September 2025 edition also allows it on arguments and input fields, as in limit above. Deprecated elements still work, are hidden from introspection unless the client asks with includeDeprecated: true, and are flagged by editors and linters.

Removing with evidence

The loop is: add the replacement, deprecate the old field with a reason and a date, tell client owners, then measure. Servers can record which fields each operation selects; a registry of persisted or trusted documents tells you exactly which shipped clients use what. When usage reaches zero (or the remaining callers have been contacted and the date has passed), remove the field. Mobile apps make this slower, because old versions stay installed for months.

Automate the check. On every schema change in CI, compare the new schema with the old one, list breaking changes, and test them against the operations clients have registered. That is contract testing with the schema as the contract: the change fails the build, not a customer's app.

2

Check yourself

3 questions. Get them all right to finish this topic.

  1. 1.Which change is safe for existing clients?
  2. 2.`email: String!` becomes `email: String`. Who can break?
  3. 3.In the September 2025 edition of the spec, where can `@deprecated` be used?

0 of 3 right so far. Get all three to finish this topic.