API design has seen an ongoing debate since 2015: GraphQL or REST? Both have deep production track records; the difference is philosophical. This article compares the two approaches with concrete criteria and shows which one fits which scenario.
The Core Difference
REST: resource-oriented (/users/42, /users/42/orders), HTTP verbs (GET/POST/PUT/DELETE), each endpoint returns a fixed response shape. GraphQL: a query language — the client says which fields it wants and the server returns exactly those. A single endpoint (/graphql), with queries sent over POST.
# REST
GET /users/42
→ { id, name, email, phone, address, createdAt, ... }
GET /users/42/orders
→ [{ id, total, items, createdAt }, ...]
# GraphQL
POST /graphql
query {
user(id: 42) {
name
email
orders(last: 10) {
id
total
}
}
}
→ { data: { user: { name, email, orders: [...] } } }
GraphQL Advantages
- No over-fetching: a mobile app needing only
nameandavatardoesn't download a 50-field user object - No under-fetching: fetch user + orders + items in a single request instead of three round trips
- Strongly typed schema: type safety via SDL (Schema Definition Language)
- Tooling: Apollo Studio, GraphiQL — auto-complete and auto-generated docs
- Subscriptions: real-time updates over WebSocket
GraphQL Drawbacks
- Caching is hard: single POST endpoint — HTTP caches don't help, you need a client-side cache
- N+1 problem: naïve resolvers hammer the DB — DataLoader is a must
- Complexity: schema, resolvers, data sources — more moving parts than REST
- Monitoring is harder: 1,000 distinct queries through a single endpoint — no route-based metrics
- Uncontrolled query cost: a malicious deeply-nested query can take the server down
REST Advantages
- Simple: every developer knows it, the learning curve is low
- HTTP semantics: status codes, methods, headers — built-in
- Caching is easy:
Cache-Control, CDN and browser cache just work - Tool ecosystem: curl, Postman, OpenAPI, SDK generators
- Versioning is easy:
/v1/users,/v2/users
The N+1 Problem and DataLoader
// Naïve resolver — N+1
const resolvers = {
Post: {
author: (post) => db.users.findByPk(post.authorId)
}
};
// Listing 100 posts triggers 100 separate user queries → DB melts
// DataLoader for batching + caching
const DataLoader = require('dataloader');
const userLoader = new DataLoader(async (ids) => {
const users = await db.users.findAll({ where: { id: ids } });
return ids.map(id => users.find(u => u.id === id));
});
const resolvers = {
Post: {
author: (post) => userLoader.load(post.authorId)
}
};
// 100 posts → a single DB query (WHERE id IN [...])
Schema Design (SDL)
type User {
id: ID!
email: String!
name: String
posts(limit: Int = 10, after: ID): PostConnection!
role: Role!
}
enum Role { USER ADMIN MODERATOR }
type Post {
id: ID!
title: String!
content: String!
author: User!
createdAt: DateTime!
}
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
}
type PostEdge {
node: Post!
cursor: String!
}
type Query {
me: User
user(id: ID!): User
posts(limit: Int = 10): [Post!]!
}
type Mutation {
createPost(input: CreatePostInput!): Post!
deletePost(id: ID!): Boolean!
}
input CreatePostInput {
title: String!
content: String!
}
Query Cost Controls
// Query depth limit
const depthLimit = require('graphql-depth-limit');
const server = new ApolloServer({
typeDefs, resolvers,
validationRules: [depthLimit(5)] // max depth of 5
});
// Query complexity
const { createComplexityLimitRule } = require('graphql-validation-complexity');
validationRules: [createComplexityLimitRule(1000)];
// Persisted queries — only whitelisted queries are accepted
const persistedQueries = { 'abc123': 'query { me { id } }' };
Which One, When?
Pick GraphQL when:
- Mobile app + web app need different data from the same backend
- Lots of nested resources: user → orders → items → products → reviews
- Large frontend team: let them write their own queries without backend changes
- Real-time features via subscriptions
- In a BFF (backend-for-frontend) pattern
Pick REST when:
- Public API: consumed by third-party integrators — REST is universal
- Simple CRUD: admin panels, dashboards — GraphQL is overkill
- CDN caching is critical: public content, high traffic
- Service-to-service microservices: REST/gRPC tend to be better
- The team's experience is in REST
Hybrid Approach
Most modern apps use both: GraphQL for the client-facing API, REST/gRPC for internal microservice communication. In the BFF pattern, a GraphQL gateway calls REST services behind it.
Alternative: tRPC
If you're in a TypeScript monorepo and don't want GraphQL's complexity but still want REST's flexibility, check out tRPC. No schema — TypeScript types give you end-to-end type safety. It's Apollo's lightweight rival.
Conclusion
"Which is better?" is the wrong question — both are modern and production-ready. When ecosystem and team experience are neutral: pick GraphQL for internal API + rapid product iteration, REST for public API + simple CRUD. Also consider tRPC for TypeScript monorepos.
Reach out to KEYDAL for GraphQL, REST or tRPC selection and API architecture. Contact us