How to Migrate from REST to GraphQL (2026)
Migrating from REST to GraphQL doesn't require a big-bang rewrite. The best approach is incremental: add a GraphQL layer that wraps your existing REST endpoints, then gradually shift. Here's the practical guide.
Should You Actually Migrate?
Before starting, make sure GraphQL solves a real problem:
✅ Migrate When
- Clients need flexible data fetching (mobile vs web need different fields)
- You have many REST endpoints that clients stitch together
- Over-fetching is causing performance issues
- You want real-time subscriptions
- Multiple frontend teams consume your API differently
❌ Don't Migrate When
- Your REST API is simple (< 10 endpoints)
- You only have one client (one frontend)
- File uploads are your primary concern
- You need maximum caching (REST + CDN is simpler)
- Your team doesn't know GraphQL
The Incremental Migration Strategy
Phase 1: GraphQL Gateway (Week 1)
Add a GraphQL server that wraps your existing REST endpoints. No backend changes needed.
Client → GraphQL Server → Your REST API → Database
Set up Apollo Server (or Yoga/Mercurius):
import { ApolloServer } from '@apollo/server'
import { startStandaloneServer } from '@apollo/server/standalone'
const typeDefs = `
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
body: String!
}
type Query {
user(id: ID!): User
users: [User!]!
}
`
const resolvers = {
Query: {
user: async (_, { id }) => {
// Call your existing REST API
const res = await fetch(`https://api.example.com/users/${id}`)
return res.json()
},
users: async () => {
const res = await fetch('https://api.example.com/users')
return res.json()
},
},
User: {
posts: async (user) => {
// Resolve nested data from another REST endpoint
const res = await fetch(`https://api.example.com/users/${user.id}/posts`)
return res.json()
},
},
}
const server = new ApolloServer({ typeDefs, resolvers })
const { url } = await startStandaloneServer(server)
Result: Clients can use GraphQL immediately. Your REST API is untouched.
Phase 2: Move Clients to GraphQL (Weeks 2-4)
Update frontend code to use GraphQL instead of REST:
Before (REST):
const user = await fetch('/api/users/1').then(r => r.json())
const posts = await fetch('/api/users/1/posts').then(r => r.json())
After (GraphQL):
const { data } = await client.query({
query: gql`
query GetUser($id: ID!) {
user(id: $id) {
name
email
posts {
title
}
}
}
`,
variables: { id: '1' },
})
One request instead of two. Only the fields you need.
Phase 3: Direct Database Access (Weeks 4-8)
Once clients use GraphQL, gradually replace REST-wrapping resolvers with direct database queries:
const resolvers = {
Query: {
user: async (_, { id }, { db }) => {
// Direct database query instead of REST call
return db.query('SELECT * FROM users WHERE id = $1', [id])
},
},
}
Phase 4: Deprecate REST (Month 2+)
Once all clients use GraphQL:
- Mark REST endpoints as deprecated
- Monitor usage — wait until REST traffic drops to zero
- Remove REST endpoints
- Simplify architecture
Schema Design Tips
Start with Your UI
Design your GraphQL schema based on what your UI needs, not your database structure.
Bad (mirrors database):
type UserRow {
user_id: Int!
first_name: String!
last_name: String!
created_at: String!
}
Good (mirrors UI needs):
type User {
id: ID!
displayName: String!
memberSince: DateTime!
}
Use Connections for Pagination
type Query {
users(first: Int, after: String): UserConnection!
}
type UserConnection {
edges: [UserEdge!]!
pageInfo: PageInfo!
}
type UserEdge {
node: User!
cursor: String!
}
Keep Mutations Clear
type Mutation {
createUser(input: CreateUserInput!): CreateUserPayload!
updateUser(id: ID!, input: UpdateUserInput!): UpdateUserPayload!
}
input CreateUserInput {
name: String!
email: String!
}
type CreateUserPayload {
user: User
errors: [Error!]
}
Solving Common Problems
N+1 Queries
When resolving nested data, each parent triggers a separate query for children. Use DataLoader:
import DataLoader from 'dataloader'
const postLoader = new DataLoader(async (userIds) => {
const posts = await db.query('SELECT * FROM posts WHERE user_id = ANY($1)', [userIds])
return userIds.map(id => posts.filter(p => p.userId === id))
})
const resolvers = {
User: {
posts: (user) => postLoader.load(user.id),
},
}
DataLoader batches individual loads into a single query. Essential for performance.
Authentication
Pass auth context to resolvers:
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => ({
user: verifyToken(req.headers.authorization),
}),
})
File Uploads
GraphQL isn't great for file uploads. Keep a REST endpoint for uploads:
app.post('/api/upload', multer().single('file'), handleUpload)
Reference uploaded files in GraphQL mutations by URL or ID.
Caching
REST caching (CDN, HTTP cache headers) is simpler than GraphQL caching. For GraphQL:
- Use persisted queries (hash queries for cacheability)
- Apollo Client cache (in-memory, normalized)
- CDN caching with
@cacheControldirective
Technology Choices (2026)
| Component | Recommended | Alternative |
|---|---|---|
| Server | GraphQL Yoga | Apollo Server, Mercurius |
| Client (React) | urql | Apollo Client |
| Schema | Code-first (Pothos) | SDL-first |
| Codegen | graphql-codegen | gql.tada |
Pothos (code-first schema) is now preferred over SDL for TypeScript projects — full type safety without codegen.
FAQ
How long does migration take?
Phase 1 (gateway): 1 week. Full migration: 1-3 months depending on API size.
Can I run REST and GraphQL simultaneously?
Yes, and you should. Run both during migration. Deprecate REST only when no clients use it.
Is GraphQL always better than REST?
No. REST is simpler, better cached, and sufficient for many APIs. GraphQL shines when clients need flexible data fetching.
What about tRPC?
If you control both client and server (TypeScript monorepo), tRPC is often better than GraphQL. GraphQL is better when you have multiple clients or public APIs.
Should I use Apollo or alternatives?
Apollo is the most popular but heaviest. For 2026: GraphQL Yoga (server) + urql (client) is the lighter, modern stack.
Bottom Line
Migrate incrementally. Start with a GraphQL gateway wrapping REST, move clients over, then optimize resolvers. Don't rewrite everything at once. The gateway approach lets you ship GraphQL to clients in a week while keeping your existing backend stable.