GraphQL vs REST in 2026
Comparing GraphQL and REST APIs in modern application development.
The GraphQL vs REST debate has been running for years, but most comparisons are theoretical. We migrated three production services from REST to GraphQL over 18 months, and the reality is more nuanced than either side claims.
Here’s what actually happened, with data.
We migrated three services with different characteristics:
| Service | REST Endpoints | Data Relationships | Frontend Clients |
|---|---|---|---|
| User Profile | 12 | Deep (user → teams → projects → tasks) | 4 (web, iOS, Android, partner) |
| Product Catalog | 8 | Moderate (product → variants → inventory) | 3 (web, mobile, internal) |
| Analytics | 25 | Flat (mostly aggregations) | 2 (dashboard, API consumers) |
The User Profile service was the biggest win. Our REST API had an endpoint that returned 47 fields, but the mobile app only needed 8.
# GraphQL: client requests exactly what it needsquery GetUserProfile($id: ID!) { user(id: $id) { name email avatar teams { name projects { name status } } }}This reduced mobile payload size by 73% and eliminated the need for 5 specialized REST endpoints (/users/:id/mobile, /users/:id/light, etc.).
With REST, each client needed different endpoints or query parameters to get the right data shape. With GraphQL, each client writes its own query:
REST approach:GET /users/:id?fields=name,email,teamsGET /users/:id?include=teams,projectsGET /users/:id/mobileGET /users/:id/admin
GraphQL approach:POST /graphql { query: "{ user(id: $id) { name email teams { name } } }" }POST /graphql { query: "{ user(id: $id) { name email teams { name projects { name status } } } }" }The GraphQL schema serves as a contract between frontend and backend:
type User { id: ID! name: String! email: String! avatar: String teams: [Team!]! role: UserRole! createdAt: DateTime!}
enum UserRole { ADMIN MEMBER VIEWER}Type generation tools (GraphQL Codegen) produce TypeScript types automatically, eliminating a whole class of frontend bugs.
GraphQL’s flexibility makes it easy to create inefficient queries. A single GraphQL request can trigger dozens of database queries:
# This could trigger 1 + N + N*M queriesquery { users { name teams { name projects { name } } }}We solved this with DataLoaders — a batching and caching layer:
from graphql import DataLoader
class TeamLoader(DataLoader): async def batch_load(self, user_ids: list[str]) -> list[list[Team]]: # Single query for all teams teams = await db.query( "SELECT * FROM teams WHERE user_id = ANY($1)", user_ids ) # Group by user_id return group_by(teams, 'user_id')DataLoaders reduced our database query count by 85% on complex GraphQL queries.
REST benefits from HTTP caching out of the box. GraphQL runs over POST, so you lose CDN caching, ETags, and browser cache.
We implemented application-level caching at the resolver level:
class Query: @staticmethod async def resolve_user(obj, info, id: str): cache_key = f"user:{id}" cached = await cache.get(cache_key) if cached: return cached
user = await db.get_user(id) await cache.set(cache_key, user, ttl=300) return userThis works but requires manual cache key management for every resolver — something REST handles automatically with URL-based caching.
Our Analytics service had 25 REST endpoints, mostly returning pre-computed aggregations. Migrating it to GraphQL added complexity without benefit:
- Queries were simple (no nested relationships)
- Clients always needed the full response (no over-fetching)
- Caching was critical (HTTP caching worked perfectly with REST)
We rolled back the Analytics migration after three months.
| Criteria | Choose GraphQL | Choose REST |
|---|---|---|
| Multiple client types | ✅ | ⚠️ |
| Deep, nested data relationships | ✅ | ❌ |
| Clients need flexible data shapes | ✅ | ❌ |
| Simple CRUD operations | ❌ | ✅ |
| HTTP caching is important | ❌ | ✅ |
| File uploads | ❌ | ✅ |
| Pre-computed aggregations | ❌ | ✅ |
| Strong typing is valued | ✅ | ⚠️ |
After the migration, our architecture looks like:
Frontend → GraphQL Gateway → [ User Service (GraphQL) Product Service (GraphQL) Analytics Service (REST) ← rolled back Payment Service (REST) ← never migrated, no need]The GraphQL gateway (using Apollo Federation) composes multiple services into a single schema. Services that benefit from GraphQL use it; services that don’t stay on REST.
- GraphQL shines with nested data and multiple clients — User Profile was the perfect use case
- Don’t migrate everything — Analytics was worse with GraphQL
- DataLoaders are mandatory — without them, you’ll kill your database
- Plan your caching strategy before migrating — you lose HTTP caching for free
- GraphQL is not a REST replacement — it’s a complement for specific use cases
Related Posts
Microservice Communication Patterns
Comparing synchronous vs asynchronous communication patterns for microservices.
Database Sharding Strategies
Scaling databases with horizontal partitioning techniques.
Zero-Downtime Database Migrations
Strategies for running database migrations without service interruptions.