REST or GraphQL? A Real Comparison of Two API Design Philosophies
When Facebook open-sourced GraphQL in 2015, many developers wrote "REST is dead." Ten years later, both are alive and widely used. So it is not that simple.
Both are valid API design approaches. But they solve different problems. Understanding when each makes sense is more valuable than ranking one above the other.
REST's Problem: Over-fetching and Under-fetching
Over-fetching: You get more data than you need.
For a user list page, you only need id, name, avatar. But GET /users returns email, phone, address, preferences, created_at, last_login, bio, and dozens more. Unnecessary data transported, parsed, loaded into memory.
Under-fetching: Multiple requests needed for the data you want.
Building a user profile page requires GET /users/123, GET /users/123/posts, GET /users/123/followers — three separate requests, three round-trips.
// Profile page with REST - 3 separate requests const [userData, postsData, followersData] = await Promise.all([ fetch("/api/users/123").then(r => r.json()), fetch("/api/users/123/posts").then(r => r.json()), fetch("/api/users/123/followers").then(r => r.json()) ]);
GraphQL: The Client Shapes the Data
In GraphQL, the client specifies exactly what data it wants. The server returns exactly that — no more, no less.
# Same profile page with GraphQL - single request query GetUserProfile($userId: ID!) { user(id: $userId) { id name avatar posts(limit: 5) { id title createdAt likesCount } followersCount } }
Single HTTP request. Only requested fields returned.
Core GraphQL Concepts
Schema: The contract of the GraphQL API. What types exist, what queries are possible — all defined in the schema.
type User { id: ID! name: String! email: String! avatar: String posts: [Post!]! followersCount: Int! } type Post { id: ID! title: String! content: String! author: User! createdAt: String! } type Query { user(id: ID!): User users(limit: Int, offset: Int): [User!]! } type Mutation { createPost(title: String!, content: String!): Post! deletePost(id: ID!): Boolean! }
Resolver: The function that knows how to resolve each field.
const resolvers = { Query: { user: async (_, { id }, context) => context.db.users.findById(id) }, User: { posts: async (user, { limit = 10 }, context) => { return context.db.posts.findByUserId(user.id, limit); }, followersCount: async (user, _, context) => { return context.db.follows.countByUserId(user.id); } }, Mutation: { createPost: async (_, { title, content }, context) => { if (!context.user) throw new Error("Unauthorized"); return context.db.posts.create({ title, content, authorId: context.user.id }); } } };
The N+1 Problem: GraphQL's Achilles Heel
If each user's posts are queried separately while returning 10 users, 1 + 10 = 11 database queries run.
The solution: DataLoader — batches requests into a single query.
import DataLoader from "dataloader"; const postLoader = new DataLoader(async (userIds) => { const posts = await db.posts.findByUserIds(userIds); return userIds.map(id => posts.filter(p => p.authorId === id)); }); User: { posts: async (user) => postLoader.load(user.id) // Batched, single query }
Decision Guide
Choose REST when: Simple CRUD operations. Designing a public API. HTTP caching is critical. Team has no GraphQL experience.
Choose GraphQL when: Different data needs on the client side (mobile needs less, web needs more). Many interrelated entities queried in different combinations. Multiple client types (web, mobile, TV) with different data needs consuming the same API.
The Hybrid Approach
REST and GraphQL do not have to be mutually exclusive. Many large companies use both: REST for public APIs and webhooks, GraphQL for complex internal queries.
GitHub is a great example: it offers both REST API v3 and GraphQL API v4. Both actively used.
API design is problem definition before tool selection. Before asking "GraphQL or REST?", answer "what are my clients' data needs?" The answer will lead you to the right tool.