Skip to content
CodeWithNaman Blog

GraphQL vs REST in 2026

Comparing GraphQL and REST APIs in modern application development.

Backend , Engineering 3 min read

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:

ServiceREST EndpointsData RelationshipsFrontend Clients
User Profile12Deep (user → teams → projects → tasks)4 (web, iOS, Android, partner)
Product Catalog8Moderate (product → variants → inventory)3 (web, mobile, internal)
Analytics25Flat (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 needs
query 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,teams
GET /users/:id?include=teams,projects
GET /users/:id/mobile
GET /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 queries
query {
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 user

This 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.

CriteriaChoose GraphQLChoose 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.

  1. GraphQL shines with nested data and multiple clients — User Profile was the perfect use case
  2. Don’t migrate everything — Analytics was worse with GraphQL
  3. DataLoaders are mandatory — without them, you’ll kill your database
  4. Plan your caching strategy before migrating — you lose HTTP caching for free
  5. GraphQL is not a REST replacement — it’s a complement for specific use cases

Questions about API design? Find me on GitHub or Twitter.

Related Posts