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