GraphQL to REST Migration: A Practical, Low‑Risk Guide
Step-by-step guide to migrate APIs from GraphQL to REST: design mapping, caching, auth, rollout, and pitfalls to avoid.
Image used for representation purposes only.
Why Migrate from GraphQL to REST?
GraphQL and REST solve many of the same problems in different ways. GraphQL excels at client-driven data composition and single-endpoint flexibility. REST shines when you want strong HTTP semantics, cacheability, and simpler operational tooling. Teams consider moving from GraphQL to REST when:
- Most clients request similar shapes of data, making GraphQL’s field-level flexibility underused.
- You want to lean on CDN/HTTP caching, conditional requests, and observability built around URLs, methods, and status codes.
- Query cost control and authorization have become complex due to arbitrary client queries.
- You need to simplify the platform for partner developers or align with enterprise governance and API gateways optimized for REST and OpenAPI.
This guide walks you through a pragmatic, low-risk migration that preserves client value while improving operability.
Principles for a Safe Migration
- Favor evolution over revolution: migrate incrementally, with parallel runs and strong observability.
- Map intent, not syntax: translate user value from GraphQL operations to REST resources and affordances.
- Use the Strangler Fig pattern: introduce REST alongside GraphQL, move traffic feature-by-feature, and retire GraphQL only when metrics prove parity.
- Be explicit about semantics: leverage HTTP methods, status codes, caching, and content negotiation to express behavior clearly.
Phase 0: Inventory, Telemetry, and Risk Assessment
Before writing a single endpoint, understand your real usage.
-
Collect real traffic
- Enable operation-level tracing in your GraphQL server.
- Capture: operation name, variables, response size, latency, error rates, and auth context.
- Rank by call volume and business criticality (e.g., checkout, payments, identity).
-
Classify operations
- Queries → candidates for GET endpoints.
- Mutations → POST/PUT/PATCH/DELETE.
- Subscriptions → webhooks, Server-Sent Events (SSE), or long polling.
-
Identify data shapes
- Common field selections suggest canonical REST representations.
- Detect over-fetching/under-fetching patterns to guide sparse fieldsets and compound responses.
-
Non-functional requirements
- Cache behavior, rate limits, SLOs, regionalization, PII flows, and audit requirements.
Deliverable: a prioritized backlog of REST resources and behaviors to design first.
Designing Your REST Surface
Strong REST designs start with nouns (resources) and state transitions.
-
Resource modeling
- Nouns: customers, orders, items, invoices, shipments.
- Hierarchies: /customers/{id}/orders, /orders/{id}/items.
- Canonical identifiers: stable IDs in URLs; avoid opaque global IDs unless necessary.
-
Methods and idempotency
- GET for retrieval (safe, cacheable).
- POST for create and non-idempotent actions.
- PUT/PATCH for updates (define idempotency precisely).
- DELETE for deletion (idempotent side effects encouraged).
-
Status codes and errors
- 2xx for success with nuance: 200 (OK), 201 (Created), 202 (Accepted), 204 (No Content).
- 4xx for client issues: 400, 401, 403, 404, 409, 422, 429.
- 5xx for server issues.
- Error body: include a machine-readable code, human message, and trace id.
{
'error': {
'code': 'invalid_parameter',
'message': 'limit must be between 1 and 100',
'trace_id': 'b1c6...'
}
}
-
Pagination, filtering, sorting
- Prefer cursor-based pagination for large datasets: page[cursor], page[size].
- Filtering via query params: filter[customerId]=…, filter[status]=….
- Sorting: sort=-createdAt, status.
-
Sparse fieldsets and includes
- fields[orders]=id,total,createdAt
- include=items,customer to avoid chattiness while keeping discoverability.
-
Caching and conditional requests
- Use Cache-Control, ETag, Last-Modified, and conditional headers (If-None-Match) to unlock CDN and browser caches.
-
Versioning
- Start with header-based (Accept: application/vnd.company.v1+json) or URL (/v1/…).
- Reserve room for non-breaking evolution via field addition and soft-deprecations.
-
Content and formats
- Consistent JSON envelope, timestamps in RFC 3339, money in integer minor units or ISO 4217, and locale-agnostic formats.
-
Hypermedia (optional)
- Provide links for discoverability (self, related) while keeping responses compact.
Mapping GraphQL to REST: Patterns
- Queries → GET endpoints
GraphQL
query GetOrders($customerId: ID!, $limit: Int!, $cursor: String) {
orders(customerId: $customerId, first: $limit, after: $cursor) {
edges { node { id total createdAt items { id sku quantity } } }
pageInfo { hasNextPage endCursor }
}
}
REST
GET /orders?filter[customerId]=123&fields[orders]=id,total,createdAt&include=items&page[size]=20&page[cursor]=abc
Accept: application/json
- Mutations → POST/PUT/PATCH/DELETE
GraphQL
mutation AddItem($orderId: ID!, $input: AddItemInput!) {
addItem(orderId: $orderId, input: $input) { id total items { id sku quantity } }
}
REST
POST /orders/987/items
Content-Type: application/json
{
'sku': 'SKU-123',
'quantity': 2
}
For partial updates, prefer PATCH with JSON Merge Patch or JSON Patch and document exact semantics.
- Subscriptions → Webhooks or SSE
- Subscriptions to order status can become:
- Webhooks: POST to partner endpoints when status changes.
- SSE: GET /orders/events?filter[customerId]=123 with a streaming response.
- Interfaces and unions
- Use a discriminant field (type) in REST payloads or OpenAPI oneOf to model variant shapes.
- Batching and composition
- Replace GraphQL composition with either:
- Server-composed compound documents using include=… to fetch related resources.
- BFF (backend-for-frontend) layer that orchestrates multiple REST calls for a specific client.
Example: End-to-End Mapping
GraphQL create-then-read pattern:
mutation CreateOrder($input: CreateOrderInput!) {
createOrder(input: $input) { id }
}
query GetOrder($id: ID!) {
order(id: $id) { id total createdAt items { id sku quantity } }
}
REST equivalent:
POST /orders
Content-Type: application/json
{ 'customerId': '123', 'currency': 'USD' }
201 Created
Location: /orders/789
GET /orders/789?include=items&fields[orders]=id,total,createdAt
Performance and Caching
GraphQL responses are often non-cacheable at the edge due to POST and variable query shapes. With REST you can:
- Make list/detail GETs cacheable with Cache-Control: public, max-age=60, s-maxage=300.
- Use ETags for validation. Clients send If-None-Match; server responds 304 if unchanged.
- Exploit URL-based caching keys: identical URLs should yield identical representations.
- Provide bulk endpoints for analytics (e.g., POST /orders/queries:async to request a large export → 202 Accepted + polling URL).
- Avoid N+1 at the client by offering include=… and well-designed sub-resources.
Security and Authorization
- Authentication: Bearer tokens or mTLS; keep it consistent across GraphQL and REST during transition.
- Authorization: Map GraphQL field-level rules to resource- or attribute-level checks in REST; consider scopes aligned to resources (orders:read, orders:write).
- Input validation: Strong server-side validation with precise 4xx codes; prefer 422 for semantic errors.
- CORS and CSRF: For browser clients, set CORS policies and use same-site cookies or double-submit tokens as needed.
- Audit trails: Log method, path, principal, resource id, and correlation id.
Tooling for REST Success
- OpenAPI: the contract of record for endpoints, parameters, and schemas.
- Mock servers: speed up client integration.
- Code generation: produce SDKs from OpenAPI for common languages.
- Contract tests: ensure server and clients remain compatible.
- Linting and style guides: enforce consistency in naming, pagination, and errors.
OpenAPI (YAML) snippet:
openapi: 3.1.0
info:
title: Orders API
version: 1.0.0
paths:
/orders:
get:
parameters:
- name: filter[customerId]
in: query
schema: { type: string }
- name: page[size]
in: query
schema: { type: integer, minimum: 1, maximum: 100 }
responses:
'200':
description: List orders
content:
application/json:
schema:
$ref: '#/components/schemas/OrderList'
Rollout and Migration Pattern
-
Build a REST façade alongside GraphQL
- Implement high-traffic read endpoints first for quick cache wins.
- Keep the GraphQL server as an orchestrator calling your new REST services during the transition, or vice versa with an adapter.
-
Shadow and canary
- Shadow REST with production GraphQL traffic to compare payloads and performance.
- Canary small percentages of client traffic to REST; watch latency, error rates, and cache hit ratios.
-
Communicate and document
- Publish OpenAPI, changelogs, and migration cookbook examples.
- Provide SDKs and sample requests.
-
Dual-write for mutations (optional, time-bounded)
- Temporarily write via both paths to ensure data parity; remove once verified.
-
Deprecation strategy
- Announce dates early and repeatedly with absolute dates (e.g., “GraphQL endpoint EOL on February 28, 2027”).
- Provide automated warnings: deprecation headers or error messages after the freeze date.
-
Rollback plan
- Keep feature flags to route traffic back to GraphQL quickly if needed.
- Maintain operational parity (monitoring, alerts) on both paths during overlap.
Testing and Quality Gates
- Golden tests: deterministic fixtures comparing GraphQL vs REST payloads for parity.
- Consumer-driven contract tests: verify client expectations against the OpenAPI contract.
- Property-based tests: validate pagination, sorting, and filtering invariants.
- Load tests: confirm REST can handle surges and that caches behave as expected.
Observability and Reliability
- Structured logs: method, path, user id, status, latency, cache status (HIT/MISS), trace id.
- Metrics: P50/P95/P99 latency, 4xx/5xx rates, request volume per endpoint, ETag validation rate, rate-limit utilization.
- Tracing: propagate correlation ids across REST calls and downstream services.
- Error budgets and SLOs: e.g., 99.9% availability for GET /orders.
Common Pitfalls (and How to Avoid Them)
- One-to-one field mapping: Resist creating an endpoint per former GraphQL field; design coherent resources.
- Overly generic ‘/search’ endpoints: Define typed filters and predictable shapes.
- Ignoring idempotency: Specify exactly when retries are safe; provide idempotency keys for POST where appropriate.
- Pagination mismatches: Document cursor translation rules; don’t silently switch clients to offset paging for large datasets.
- Breaking changes hidden as “fixes”: Use versioning; don’t repurpose fields.
Client Migration Playbook
- Provide a migration matrix that maps GraphQL operations to REST endpoints.
- Ship example snippets for each client platform.
- Offer a compatibility adapter in SDKs that calls REST under the hood while exposing old GraphQL-like helpers during a grace period.
Example curl
curl -s \
-H 'Authorization: Bearer <token>' \
'https://api.example.com/v1/orders?filter[customerId]=123&fields[orders]=id,total,createdAt&include=items&page[size]=20'
Measuring Success
- Reduced median latency for common reads (e.g., -30%).
- High cache hit ratio at edge and origin for GETs.
- Lower error rates due to clearer validation and status codes.
- Developer adoption: SDK downloads, time-to-first-success.
- Support volume: fewer tickets related to query design.
Checklist
Pre-launch
- OpenAPI defined, linted, and reviewed
- Error model consistent and documented
- Caching strategy (ETag/Cache-Control) verified
- Auth scopes mapped and tested
- Observability dashboards ready
Launch
- Shadow traffic comparison within tolerances
- Canary ramp with SLOs guarded by feature flags
- Client SDKs and examples published
Post-launch
- Communicate deprecation milestones with absolute dates
- Monitor adoption and cache metrics
- Remove unused GraphQL operations progressively
Conclusion
Migrating from GraphQL to REST is not a step backward; it’s a strategic realignment to embrace the strengths of the web’s native application protocol. By auditing real usage, designing resourceful endpoints, leveraging HTTP semantics, and rolling out with discipline, you can reach a simpler, faster, and more cache-friendly API—without surprising your consumers. Treat the migration as a product, not just a refactor, and your developers—and your users—will feel the difference.
Related Posts
REST API Content Negotiation: A Practical Guide
Design, implement, and test REST API content negotiation: headers, q-values, media types, versioning, caching, and error handling with practical examples.
API Backward Compatibility Strategies: Designing Change Without Breaking Clients
Practical strategies to keep APIs backward compatible—versioning, additive changes, deprecation, rollout, and testing for REST, GraphQL, and gRPC.
Designing a Robust REST API Response Envelope: Patterns, Pitfalls, and Practical Examples
A practical guide to the REST API response envelope pattern: design, errors, pagination, performance tips, and when to use or skip it.