Shipping a new feature should never mean waking up to a flood of broken integrations. Yet for many teams running both REST and GraphQL endpoints, that is still the daily reality. The fix is not to pick a winner between the two. It is to treat them as a single, evolving contract and let a schema registry plus a code generation pipeline do the heavy lifting. This guide walks through a practical dual-API approach to versioning that keeps existing clients stable while giving new consumers the freshest possible surface area.
Why Dual-API Versioning Is the New Default
Most engineering organisations did not arrive at a single, blessed API style on purpose. They grew into it. REST handles batch jobs, webhooks, and legacy enterprise consumers, while GraphQL wins for mobile apps, internal dashboards, and any front-end that needs flexible shapes. Trying to force a migration in either direction is expensive and politically painful, so the realistic path forward is coexistence with discipline.
The discipline part is what trips teams up. Without a shared source of truth, REST and GraphQL drift apart, breaking changes sneak in, and consumers end up pinning to old revisions. A schema-first workflow anchored in a registry turns the two styles into two views of the same underlying model.
The Core Principle: One Model, Two Projections
Think of your domain model as the canonical layer. REST and GraphQL are simply different projections of that model, each optimised for a different consumer. The schema registry stores the canonical definitions, and code generators produce the transport-specific artefacts:
- OpenAPI or AsyncAPI documents for REST endpoints, generated from tagged entities in the registry
- GraphQL SDL files for each federated subgraph, generated from the same entities
- TypeScript, Java, Go, and Swift types for client SDKs, generated from the OpenAPI and SDL outputs
When the canonical model changes, every projection regenerates automatically. Reviewers see the full impact of a change in one pull request, which is where good API design actually happens.
Setting Up a Schema Registry That Works for Both Styles
A registry is more than a storage layer. It is the audit log, the search index, and the policy enforcer for every field your company exposes. In practice, teams choose between a hosted service and a self-hosted one, but the requirements look similar:
- Versioned entities with semver semantics, including deprecation metadata
- Field-level ownership so the right team gets pinged on a breaking change
- Schema diffs that show what is additive, what is breaking, and what is just a rename
- CI hooks that block merges when a change violates the published compatibility rules
For GraphQL, the registry can hold SDL directly. For REST, store the canonical entity definitions and let the pipeline emit OpenAPI. The key is to never hand-write the transport files. Hand-written schemas are the leading cause of accidental breakage.
Designing a Codegen Pipeline That Runs on Every Merge
The pipeline is the part that pays for the whole setup. A typical flow looks like this:
- A developer edits a canonical entity, such as
Customer, in the registry - A pull request triggers a dry-run generation of both the OpenAPI and GraphQL artefacts
- Automated checks flag breaking changes against the last published revision
- If approved, the pipeline publishes the new schemas to a consumer portal and rebuilds the SDK packages
Because the output is deterministic, you can cache artefacts, snapshot them in tests, and replay any historical revision. That last capability is gold for debugging consumer reports like, “It worked on Tuesday but broke on Wednesday.”
Backward-Compatible Patterns That Actually Work
Even with great tooling, you still need a playbook for safe evolution. The patterns below have held up across hundreds of production APIs:
For REST
- Add new fields with safe defaults rather than repurposing existing ones
- Use additive URL versioning for breaking routes, such as
/v2/orders, while keeping/v1/orderson life support - Treat query parameters as a contract and lint them in CI
- Deprecate, do not delete: set a sunset header and announce timelines in the registry
For GraphQL
- Never remove a field without a deprecation period of at least six months
- Add new fields at the type level so existing queries keep working
- Introduce union types for evolving responses instead of switching return shapes
- Use persisted queries on the client side to lock consumers to a known shape
The principle is the same in both worlds: add, deprecate, and only remove after a measurable migration window.
Communicating Changes Without Spamming Consumers
One underrated benefit of a schema registry is structured change communication. Instead of mailing a PDF, you publish a changelog that consumers can subscribe to via RSS, webhook, or GraphQL subscription. Each entry includes the affected field, the impact level, the migration deadline, and a generated diff that humans can actually read.
Pair this with consumer-driven contract tests. Major clients submit a “pinned” snapshot of the schema fields they depend on, and CI blocks the release if any of those fields change without a deprecation flag. This is how Netflix, Shopify, and GitHub have kept gigantic public APIs stable for years.
Common Pitfalls When Running the Two Side by Side
Dual-API versioning is not free. A few failure modes show up repeatedly in post-mortems:
- Two sources of truth. If the OpenAPI file and the GraphQL SDL are edited by hand in addition to the registry, the registry becomes a lie. Pick one entry point and enforce it.
- Over-versioning. Adding
/v3before/v2is sunset creates maintenance debt. Prefer additive evolution inside a version. - Ignoring the federation boundary. In a federated GraphQL setup, each subgraph is its own contract. Treat every subgraph schema as a publishable artefact with its own compatibility rules.
- Skipping SDK regen. If the generated SDKs lag behind the published schemas, clients will work around the gap and pin to stale types.
Measuring Success: The Metrics That Matter
You cannot improve what you do not measure. Track these numbers quarterly and you will see the strategy working:
- Time-to-publish from schema change to consumer availability, ideally under one hour
- Breaking change lead time, meaning how early in the development cycle breaking changes are caught
- Deprecated field usage, showing how many clients are still hitting sunset paths
- SDK adoption rate, the percentage of consumers using generated clients versus hand-rolled ones
When these trends move in the right direction, you have evidence that the dual-API approach is paying for itself.
Conclusion
Versioning REST and GraphQL together is less about picking a versioning scheme and more about creating a single, governed source of truth that feeds both styles. A schema registry captures the canonical model, a codegen pipeline produces consistent transport artefacts, and consumer-driven contracts keep everyone honest. The result is an API platform that can evolve at the speed of product without leaving its existing clients behind.
