Skip to content
advanced Phase 11 · Error Handling, Nullability, and Schema Evolution

Operating a Versionless GraphQL API

Why GraphQL avoids URL versions and how teams manage change over years.

45m
1 problems
Topic Progress 0%

Why /v2 Never Ships

Why /v2 Never Ships

Versioned REST has a familiar ritual: freeze v1, build v2, run both, nag clients to migrate, and eventually discover that a warehouse integration still calls v1 four years later. The URL version is cheap to create and expensive to retire — every day it lives, someone must backport fixes and document two of everything.

GraphQL's alternative is a single endpoint whose schema evolves in place. The mechanism is field-level granularity: a client depends only on the fields and arguments it selects, so the surface you must preserve for that client is the ~15 fields it uses, not the whole API. Adding anything is free, and removal follows the runway described in the schema evolution rules topic — deprecate, measure, gate, delete.

The fork you avoid is concrete. A second version means:

  • Two schemas to compose, two sets of resolvers to maintain, two documentation sets — and every new feature either lands twice or waits for a backport.
  • Two codegen outputs per client, with developers copying types across the boundary because the "wrong" version's types leak into a shared package.
  • Gateway routing and caching rules keyed by path, plus support burden split across both.
  • No natural end date: without telemetry proving zero traffic, the old endpoint becomes permanent infrastructure.

One clarification that interviews reward: nothing in the GraphQL specification forbids /v2/graphql. Versionlessness is an operational strategy that teams choose because they have the machinery to make it safe — not a property the protocol confers. A team without usage data and CI checks is not running a versionless API; they are running an unversioned one, which is a different and worse thing.

The Machinery Behind Safe Continuous Change

The Machinery Behind Safe Continuous Change

Continuous evolution only works while five components stay in place. Take one away and the schema stops being safe to touch.

1. Ownership. Schema files under CODEOWNERS, with a design review for anything that touches public types. A versionless API has no release train to hide behind, so the review that used to happen at "v2 design time" moves into every schema pull request. Ownership also answers the question nobody asks until it hurts: who decides that a deprecated field is ready to die.

2. Registered operations. Clients submit their operation documents — persisted queries, checked-in operation artifacts, or generated operation manifests from codegen builds. Registration is what turns "someone might use this field" into "three operations in two repos use it." Unregistered traffic (see the blind spot below) is handled by sampling raw query text at the gateway.

3. Field usage telemetry. Per-field, per-client request counts and recency:

Metric Answers
requestedBy (count of operations) How many callers would break if removed
lastRequestedAt Whether anyone still cares
requestsPerDay Blast radius if it does break
clients[] Who to contact before removal

4. A written deprecation policy. How long a field lives between @deprecated and deletion, where the announcement lands, who signs off. Without a written window, deprecations accumulate: schemas rot into museums of fields with zero traffic because deleting anything felt like a decision requiring a meeting.

5. Enforcement in CI. The rule that blocks merges that remove fields still present in registered operations. Everything above is measurement; this is the point where measurement has consequences. The build mechanics are in schema checks in CI.

The blind spot to design around: telemetry sees only what you instrument. Third-party integrations sending ad hoc queries, cached scripts, and anything that bypasses your operation registry are invisible. Mitigations: sample raw query documents at the gateway, log field paths server-side for a percentage of requests, and treat "zero usage" as "zero observed usage" — which is why removal notices go out even when the dashboard reads zero.

When a Second Endpoint Is the Honest Answer

When a Second Endpoint Is the Honest Answer

Evolution has a limit: you can only add your way out of a problem if the old meaning can keep existing. Some changes cannot be expressed additively, and pretending otherwise produces a schema that lies.

Justify a new endpoint when:

  • Semantics must change on an existing field. orders(limit: Int) becomes cursor-paginated with different semantics: both behaviors cannot live under one name honestly — adding ordersAfter(cursor) works only if the old shape can eventually be deleted.
  • The auth or tenancy model changes. Different scoping rules per request are a different API, and clients written against the old assumptions would silently over-fetch or under-fetch.
  • Cost/billing resets. Metering new economics onto old field names makes invoices impossible to explain.
  • The unmeasurable population is large. If telemetry cannot see a significant share of traffic (public third-party API with anonymous callers), the "measure to zero" runway never completes, and a versioned surface with its own contract is safer than a silent break.
  • A protocol-level feature changes request handling. Batch semantics, file uploads, or a new transport that old gateways cannot parse.

Running both during migration:

  1. Keep /graphql (v1) frozen: additive-only, no new fields, explicit sunset date.
  2. Ship /v2/graphql with the reworked schema; register operations separately so usage is measured per version.
  3. Migrate client teams by app version, watching v1 traffic as the burn-down chart.
  4. Kill v1 when traffic stays at zero for a defined window — the same telemetry discipline, applied to a whole endpoint instead of one field.

The tradeoff is honest and one-directional: a fork doubles schema surface, docs, codegen, and support, so it buys time rather than avoiding work. Adopt it when coordination with unmeasurable clients outweighs the doubling cost — and put the sunset date in the first announcement, or the fork becomes permanent.

Interview framing: answer in layers. First: GraphQL prefers field-level evolution with deprecation and telemetry, because clients only depend on what they select. Second: enforcement comes from schema checks in CI. Third: a new endpoint remains legitimate for semantic resets and unmeasurable client populations — and you would still run it with a published sunset date and per-version usage data.

Practice Problems

0 / 1 solved
Plan a Field Removal from Telemetry
Deprecation runway

Analytics.onboardedAt (deprecated 6 weeks ago, reason points to analytics.firstSeenAt) shows the usage below. Decide whether removal may merge now, what must happen first, and write the gate that enforces the decision in CI.

Example:

Input: lastRequestedAt: 2 days ago · requestingOperations: 3 (2 in mobile-app@4.x, 1 in nightly-report.py) · requestsPerDay: ~40

Output: Do not remove: contact owners of the two operations, migrate to firstSeenAt, re-measure; nightly-report.py is unregistered-looking traffic to chase directly

Recent activity in three documents means the check would fail — removal now converts a deprecated field into production validation errors for real callers.

Optimal Solution — O(1) time, O(1) space

Treat removal as a predicate over two usage sources rather than a calendar: any requesting operation or any recent raw-text mention blocks the merge; silence across both for a defined window allows it, provided the announcement already went out.

// gate.ts — runs inside the schema-check job
type Usage = {
  lastRequestedAt: string | null;   // from operation registry
  requestingOperations: number;
  rawSampleMentions30d: number;     // gateway query-text sampling
  announcedAt: string | null;       // changelog / client notice
  deprecatedSince: string;
};

function mayRemove(u: Usage): { ok: boolean; reason: string } {
  if (!u.announcedAt) return { ok: false, reason: 'no removal notice published' };
  if (u.requestingOperations > 0) return { ok: false, reason: 'registered operations still select it' };
  if (u.rawSampleMentions30d > 0) return { ok: false, reason: 'unregistered traffic still mentions it' };
  const quietDays = daysSince(u.lastRequestedAt ?? u.deprecatedSince);
  if (quietDays < 90) return { ok: false, reason: 'usage window not silent: ' + quietDays + 'd' };
  return { ok: true, reason: 'zero observed usage for ' + quietDays + 'd across both instruments' };
}

Edge Cases:

  • Never observed at all (no telemetry baseline): fall back to the deprecation window plus a public notice
  • Registry says zero but raw sampling shows hits (third-party scripts)
  • Announcement published after deprecation rather than before (order is not proof — require both)

Quiz

1. What makes cleanup of deprecated fields possible in a versionless API?

Question 1 options

2. A schema has 400 fields; a client selects 15. Why does that matter for versioning?

Question 2 options

3. When is a second GraphQL endpoint the defensible choice?

Question 3 options

4. What population does field usage telemetry fail to see?

Question 4 options

Flashcards

Question

Schema-as-version

Answer

One endpoint whose schema evolves in place; clients depend on the fields they select, not on a URL generation.

Question

Five components of safe evolution

Answer

Ownership, registered operations, field usage telemetry, written deprecation policy, CI enforcement.

Question

Telemetry blind spot

Answer

Unregistered ad hoc queries are invisible — sample raw query text at the gateway and treat zero as zero-observed.

Question

Justify a second endpoint

Answer

Semantics of an existing field must change, or an unmeasurable client population prevents a deprecation runway from completing.

Question

Why a fork is expensive

Answer

Two schemas, resolvers, docs, codegen outputs, gateway routes — and no natural end date without sunset telemetry.

Question

Versionless does not mean unversioned

Answer

Without telemetry and CI gates, schema changes are uncoordinated risk, not a strategy.

Revision Notes

Key Takeaways

  • 1. Clients depend on selections, which is why additive evolution replaces URL versioning
  • 2. Versionless requires machinery: ownership, registration, telemetry, policy, enforcement
  • 3. Telemetry cannot see unregistered callers; pair dashboards with raw query sampling
  • 4. A second endpoint is for semantic resets and unmeasurable populations — always with a sunset date

Interview Tips

  • • Answer "how do you version GraphQL API?" in layers: field evolution, deprecation runway, CI checks, endpoint fork as last resort
  • • Name the concrete artifacts (CODEOWNERS, operation registry, usage dashboard, check failure) rather than the abstract principle
  • • Be ready to defend when you would *not* fork — interviewers probe whether the strategy has any discipline behind it

Cheat Sheet

Versionless API Cheat Sheet

Model: one endpoint, schema evolves in place; clients depend only on selected fields.

Required machinery

  1. CODEOWNERS + schema design review on every PR
  2. Registered operations (persisted queries / codegen manifests)
  3. Field usage telemetry: requestedBy, lastRequestedAt, clients[]
  4. Written deprecation window + announcement channel
  5. CI check blocking removal of fields with active operations

Blind spot: unregistered ad hoc callers — sample raw query text; call it "zero observed usage".

Fork only when: existing field semantics must change, or clients cannot be measured/coordinate.
Fork rules: freeze old version (additive only), register ops per version, sunset date announced up front, kill when traffic = 0 for a defined window.

Enforcement details: schema checks in CI · Removal mechanics: schema evolution rules

Continue Learning