← Back to articles

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:

  1. Mark REST endpoints as deprecated
  2. Monitor usage — wait until REST traffic drops to zero
  3. Remove REST endpoints
  4. 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 @cacheControl directive

Technology Choices (2026)

ComponentRecommendedAlternative
ServerGraphQL YogaApollo Server, Mercurius
Client (React)urqlApollo Client
SchemaCode-first (Pothos)SDL-first
Codegengraphql-codegengql.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.

Get AI tool guides in your inbox

Weekly deep-dives on the best AI coding tools, automation platforms, and productivity software.