Back to Articles
Backend Feb 21, 2026 · 14 min read

How I Migrated from REST to GraphQL Without Breaking Production

Daniel

Software Developer

Our REST API had 15 endpoints. The frontend needed data from 4 of them to render a single page. That meant 4 HTTP requests, 4 round trips, and a slow user experience. I migrated to GraphQL over 3 weeks. Here is exactly how I did it — and the one mistake that almost took down production.

Why GraphQL? The Real Reason

It wasn’t about being trendy. It was about performance. Our product page made 6 REST calls:

  • GET /api/products/123 — product details
  • GET /api/products/123/reviews — reviews
  • GET /api/products/123/related — related products
  • GET /api/inventory/123 — stock status
  • GET /api/pricing/123 — current price
  • GET /api/users/me — user preferences

Total time: ~800ms on a good day. With GraphQL, this became 1 request: ~200ms.

Step 1: Define the Schema (Day 1-2)

I started with the schema. Not the code — the schema. This is the contract between frontend and backend.

type Product {
id: ID!
name: String!
description: String
price: Float!
inventory: Int!
reviews: [Review!]!
relatedProducts: [Product!]!
}

type Review {
id: ID!
rating: Int!
comment: String
author: String!
createdAt: String!
}

type Query {
product(id: ID!): Product
products(limit: Int, offset: Int): [Product!]!
}

Key decision: I made reviews and relatedProducts fields on Product rather than separate queries. This is the GraphQL advantage — the client asks for exactly what it needs.

Step 2: Build the Resolvers (Day 3-7)

Each field in the schema needs a resolver. I used graphql-yoga with Prisma:

const resolvers = {
Query: {
product: async (_, { id }, { prisma }) => {
return prisma.product.findUnique({ where: { id } });
},
products: async (_, { limit, offset }, { prisma }) => {
return prisma.product.findMany({
  take: limit,
  skip: offset,
});
},
},
Product: {
reviews: async (parent, _, { prisma }) => {
return prisma.review.findMany({
  where: { productId: parent.id },
});
},
relatedProducts: async (parent, _, { prisma }) => {
return prisma.product.findMany({
  where: {
    category: parent.category,
    id: { not: parent.id },
  },
  take: 4,
});
},
},
};

The N+1 problem hit me immediately. For 10 products, GraphQL would make 10 separate queries for reviews. The fix: DataLoader.

import DataLoader from 'dataloader';

const reviewLoader = new DataLoader(async (productIds) => {
const reviews = await prisma.review.findMany({
where: { productId: { in: productIds } },
});

return productIds.map(id =>
reviews.filter(r => r.productId === id)
);
});

Now 10 products = 1 query for reviews instead of 10.

Step 3: The Strangler Pattern (Day 8-14)

I didn’t replace REST all at once. I used the strangler pattern: put a proxy in front of the old API and gradually route traffic to GraphQL.

// Middleware: route GraphQL requests
app.use('/api/graphql', graphqlHandler);

// Keep REST endpoints working
app.use('/api/products', restProductHandler);
app.use('/api/reviews', restReviewHandler);

// Gradually update frontend to use /api/graphql

Both APIs ran simultaneously. The frontend could use either. This meant zero downtime.

The Mistake That Almost Broke Production

On day 10, I deployed a resolver that didn’t have a depth limit. A user sent this query:

query {
products {
relatedProducts {
relatedProducts {
  relatedProducts {
    relatedProducts {
      ... infinite loop
    }
  }
}
}
}
}

The server tried to resolve 4^10 = 1,048,576 products. CPU hit 100%. The site went down for 3 minutes.

The fix: depth limiting and query cost analysis.

import depthLimit from 'graphql-depth-limit';

const server = new YogaServer({
validationRules: [depthLimit(5)],
});

Now any query deeper than 5 levels is rejected immediately.

Step 4: Update the Frontend (Day 15-18)

With Apollo Client on the frontend, the migration was straightforward:

// Before (REST)
const [product, setProduct] = useState(null);
const [reviews, setReviews] = useState([]);

useEffect(() => {
fetch(`/api/products/${id}`).then(r => r.json()).then(setProduct);
fetch(`/api/products/${id}/reviews`).then(r => r.json()).then(setReviews);
}, [id]);

// After (GraphQL)
const { data, loading } = useQuery(PRODUCT_QUERY, {
variables: { id },
});

const PRODUCT_QUERY = gql`
query Product($id: ID!) {
product(id: $id) {
name
price
inventory
reviews {
  rating
  comment
  author
}
relatedProducts {
  name
  price
}
}
}
`;

One query instead of two. And if I need more data later, I just add fields — no new endpoint needed.

Results After 3 Months

MetricBeforeAfter
Avg page load time800ms200ms
API endpoints151 (with flexible queries)
Frontend network requests4-6 per page1 per page
Backend CPU usage45%35%
Developer velocitySlow (new endpoints needed)Fast (just add fields)

Should You Migrate?

Yes if:

  • Your frontend makes multiple REST calls per page
  • You have over-fetching (returning more data than needed)
  • You have under-fetching (needing multiple calls for related data)

No if:

  • Your REST API is simple (under 5 endpoints)
  • You have caching working well with CDN
  • Your team has no GraphQL experience and no time to learn

One Last Tip

Use GraphQL Playground or Apollo Studio during development. Being able to test queries interactively is a game-changer. It’s like having documentation that you can actually run.

Tags

GraphQLRESTAPIMigrationBackend