Skip to main content
Skip to main content

REST to GraphQL Migration Workflow

A reusable, phase-by-phase playbook for migrating a REST API to GraphQL with zero downtime: gradual rollout, backwards compatibility, codegen, schema versioning, and a cutover checklist.

REST to GraphQL Migration Workflow#

A Reusable Pattern, Not a Story

This page is a technical reference, distilled from a real production migration, that you can apply directly to your own REST-to-GraphQL migration. It's organized as a repeatable pattern and checklist rather than a narrative.

Reference Case#

Real-World Result: REST → GraphQL at Scale

50,000+

Developers Migrated

Entire external developer ecosystem moved to GraphQL

Zero

Downtime

Achieved through a dual-stack, gradual rollout

+40%

API Performance

Measured improvement after migration

-30%

Support Tickets

Reduction in API-related support volume

These figures come from a Bazaarvoice REST-to-GraphQL migration. See Case Studies and the longer-form write-up, REST to GraphQL: The Migration That Saved Our API, for the full story. The workflow below generalizes that migration into steps you can apply regardless of stack.

When to Use This Workflow#

GraphQL migration tends to pay off when you have:

  • Multiple client types (web, iOS, Android) with different data-shape needs from the same backend
  • Deeply nested or relational data that currently requires several chained REST calls per view
  • A large, versioned endpoint surface that's expensive to document and maintain
  • Frontend teams blocked on backend changes for every new UI data requirement
  • A big enough developer base that documentation and onboarding cost is a real line item

When to Stick with REST#

Migrating isn't automatically the right call. Stay on REST (or scope the migration down) when:

  • Your API is mostly simple CRUD with few relationships to traverse
  • It's a public API you don't control the clients for — arbitrary GraphQL queries are harder to secure and cache than fixed REST routes
  • You have heavy file upload/download workflows (GraphQL doesn't handle binary payloads natively)
  • The team has no GraphQL experience and the timeline doesn't allow for a learning curve
  • The existing REST API already works well and developer feedback is positive — don't migrate for trend's sake

The Gradual Rollout: Strangler Fig, Not Big Bang#

Replace REST endpoints incrementally while both stacks run in production side by side, rather than attempting a single cutover. This is the "Strangler Fig" pattern applied to APIs.

Foundation

Design the schema around your domain graph. Stand up a GraphQL layer whose resolvers call your existing services — no business logic moves yet. Ship developer tooling (GraphQL Playground/Explorer, auto-generated schema docs) from day one.

✓ Completed

Parallel Operation

New features ship GraphQL-only. REST stays fully supported for existing integrations. Both stacks read from the same underlying services, so behavior stays consistent.

✓ Completed

Strategic Cutover

Migrate existing REST consumers by priority (highest traffic and highest support burden first), not by convenience. Move one integration at a time and monitor before moving the next.

✓ Completed

Deprecation & Sunset

Mark migrated REST endpoints deprecated, track remaining usage, and retire them only after usage drops to zero (or an agreed floor) — never on a fixed calendar date alone.

✓ Completed

Backwards-Compatibility Strategy#

The core rule: REST and GraphQL read from the same service layer, so neither stack can drift out of sync while both are live.

JavaScript
// GraphQL resolvers delegate to the same services REST controllers already use —
// no duplicated business logic, no risk of the two APIs disagreeing.
const resolvers = {
  Query: {
    user: async (_, { id }) => userService.findById(id),
  },
  User: {
    projects: async (user) => projectService.findByUser(user.id),
  },
};

Practical rules that keep this safe:

  • Never remove or repurpose a REST endpoint until its GraphQL replacement has shipped and its usage has been migrated
  • Version by field, not by endpoint — GraphQL has no /v2/ equivalent; additive schema changes are the versioning mechanism
  • Put both stacks behind the same auth, rate limiting, and observability layer so switching costs nothing for consumers
  • Publish a side-by-side migration guide for every endpoint you move, showing the old REST call and the equivalent GraphQL query

Type-Safe Codegen Approach#

Treat the schema as the single source of truth and generate code from it in both directions:

  • Server-side: generate resolver/type signatures from the schema (e.g. GraphQL Code Generator) so resolver implementations can't drift from the contract
  • Client-side: generate typed query/mutation hooks for each client (Apollo Client for web, Apollo iOS/equivalent for mobile) instead of hand-writing fetch calls
  • CI gate: run codegen and schema-diff checks on every pull request; a PR that introduces a breaking schema change should fail CI, not get caught in production
  • Contract tests: validate that persisted/allow-listed queries from each client still resolve correctly against the current schema before deploy

Schema Versioning Without Breaking Clients#

GraphQL has one schema for all consumers, so every change has to be evaluated for whether it's additive or breaking.

ChangeSafe?Why
Add an optional field✅ SafeExisting queries don't reference it, so nothing breaks
Add a new type or query✅ SafePurely additive to the graph
Deprecate a field with @deprecated✅ SafeField still resolves; clients get a warning, not an error
Remove a field❌ BreakingAny client still selecting it fails
Rename a field❌ BreakingEquivalent to removal from the client's perspective
Change a field's type❌ BreakingExisting selections may no longer typecheck

Deprecate before you remove:

Graphql
type User {
  name: String!
  email: String! @deprecated(reason: "Use contactInfo.email instead")
  contactInfo: ContactInfo!
}
  • Track deprecated-field usage in production analytics before scheduling removal
  • Keep a schema changelog so consumers can see what changed and when
  • Only remove a deprecated field once usage has dropped to zero (or an agreed-upon floor) for a full deprecation window

Guardrails: Performance & Reliability#

A single flexible endpoint introduces failure modes REST doesn't have — plan for them up front:

  • N+1 queries — batch and cache per-request data fetching with a loader pattern (e.g. DataLoader) so resolving a list of parents doesn't issue one query per child
  • Expensive/abusive queries — enforce query complexity or cost limits so a single query can't request unbounded nested data
  • Caching — REST caches by URL; GraphQL needs a normalized cache (e.g. Apollo's InMemoryCache) or a persisted-query allowlist in production
  • Monitoring — REST-era, per-URL dashboards don't work against one endpoint; adopt GraphQL-aware tooling (e.g. Apollo Studio or equivalent) that reports per-operation, not per-route

Migration Priority: What to Move First#

When deciding which REST endpoints to migrate next, rank by:

  1. Usage volume — highest-traffic endpoints first; they generate the most benefit per unit of migration effort
  2. Developer pain — endpoints with the most support tickets or "how do I get X" questions
  3. Maintenance burden — endpoints that change often or accumulate the most bugs
  4. Blast radius — prefer endpoints with fewer, well-known consumers before tackling ones with unknown/public callers

Cutover Checklist#

Pre-Migration Assessment

0 / 4 completed
Pull API usage analytics to identify highest-traffic endpoints
Survey developers for the most-complained-about integration points
Capture a performance baseline (latency, calls-per-view, error rate) before changing anything
Assess team GraphQL skill gaps and plan training

Migration Planning

0 / 4 completed
Design the schema around domain relationships, not existing endpoints
Build a migration priority list (usage, pain, maintenance burden)
Confirm the backwards-compatibility approach: dual-stack, shared service layer
Plan guardrails up front: DataLoader, query complexity limits, caching strategy

Implementation

0 / 5 completed
Stand up developer tooling: schema explorer, auto-generated docs
Wire codegen into CI for both server types and client operations
Add schema-diff checks to CI to catch breaking changes before merge
Set up GraphQL-aware monitoring (per-operation, not per-route)
Ship a side-by-side REST-vs-GraphQL migration guide per endpoint

Cutover & Post-Migration

0 / 5 completed
Migrate consumers by priority, one integration at a time
Mark moved REST endpoints deprecated; do not remove immediately
Track deprecated-endpoint and deprecated-field usage until it reaches zero
Retire deprecated REST endpoints only after the usage floor is met
Re-measure performance and support-ticket volume against the baseline

Decision Framework: REST vs. GraphQL#

Choosing Per-Endpoint, Not All-or-Nothing

Keep on REST

Leave this endpoint as-is for now

Best for: Simple CRUD, file transfer, public APIs with uncontrolled clients
✅ Pros:
  • •Simple, well-understood caching
  • •No client-side query risk
  • •Lower migration cost
❌ Cons:
  • •Multiple round trips for related data
  • •Over/under-fetching on mobile
  • •Endpoint sprawl over time
Migrate to GraphQL

Move this endpoint into the schema

Best for: Nested/relational data, multiple client types, high-traffic dashboards
✅ Pros:
  • •Single request for nested data
  • •Self-documenting via introspection
  • •Frontend queries evolve independently
❌ Cons:
  • •Requires N+1/complexity guardrails
  • •Needs normalized caching strategy
  • •Team learning curve
Previous
First page
Next
Last page