“Should I choose GraphQL or REST?” is a frequently debated topic in API design. Both have fundamentally different design philosophies, and the important thing is not which is better, but making the appropriate choice based on project requirements. This article aims to help you understand the differences deeply and make the right decision.
Differences in Design Philosophy
REST: Resource-Oriented Architecture
REST is a design philosophy centered around “resources.”
REST Design Principles:
/users/123- URI that uniquely identifies a User resource
| HTTP Method | Endpoint | Action |
|---|---|---|
| GET | /users/123 | Get user |
| PUT | /users/123 | Update user |
| DELETE | /users/123 | Delete user |
| POST | /users | Create user |
GraphQL: Query Language Approach
GraphQL is a design philosophy where “clients declaratively fetch the data they need.”
GraphQL Design Principles:
- Single endpoint:
/graphql - Specify required data in query:
query {
user(id: "123") {
name
email
posts(limit: 5) {
title
}
}
}
Data Fetching Pattern Comparison
Over-fetching Problem
// REST: Returns data including unnecessary fields
// GET /users/123
{
"id": "123",
"name": "John Smith",
"email": "john@example.com",
"phone": "555-1234-5678", // unnecessary
"address": "New York...", // unnecessary
"createdAt": "2024-01-01", // unnecessary
"updatedAt": "2024-12-01", // unnecessary
"settings": { ... }, // unnecessary
"profile": { ... } // unnecessary
}
// GraphQL: Fetch only required fields
// query { user(id: "123") { name, email } }
{
"data": {
"user": {
"name": "John Smith",
"email": "john@example.com"
}
}
}
Under-fetching Problem (N+1 Requests)
// REST: Multiple requests needed
// 1. GET /users/123
// 2. GET /users/123/posts
// 3. GET /posts/1/comments
// 4. GET /posts/2/comments
// ... (N+1 problem)
// GraphQL: Get all data in 1 request
query {
user(id: "123") {
name
posts {
title
comments {
content
author { name }
}
}
}
}
Schema Definition
REST: OpenAPI/Swagger
# openapi.yaml
openapi: 3.0.0
info:
title: User API
version: 1.0.0
paths:
/users/{id}:
get:
summary: Get user
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/User'
components:
schemas:
User:
type: object
properties:
id:
type: string
name:
type: string
email:
type: string
format: email
GraphQL: SDL (Schema Definition Language)
# schema.graphql
type User {
id: ID!
name: String!
email: String!
posts(limit: Int, offset: Int): [Post!]!
createdAt: DateTime!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
comments: [Comment!]!
}
type Comment {
id: ID!
content: String!
author: User!
}
type Query {
user(id: ID!): User
users(filter: UserFilter): [User!]!
post(id: ID!): Post
}
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
deleteUser(id: ID!): Boolean!
}
input CreateUserInput {
name: String!
email: String!
}
Implementation Comparison
REST Server (Express)
// REST API (Express + TypeScript)
import express from 'express';
const app = express();
app.use(express.json());
// Get user
app.get('/users/:id', async (req, res) => {
const user = await db.user.findUnique({
where: { id: req.params.id }
});
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
});
// Get user's posts
app.get('/users/:id/posts', async (req, res) => {
const posts = await db.post.findMany({
where: { authorId: req.params.id },
take: parseInt(req.query.limit as string) || 10,
skip: parseInt(req.query.offset as string) || 0
});
res.json(posts);
});
// Create user
app.post('/users', async (req, res) => {
const { name, email } = req.body;
const user = await db.user.create({
data: { name, email }
});
res.status(201).json(user);
});
GraphQL Server (Apollo Server)
// GraphQL API (Apollo Server + TypeScript)
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
const typeDefs = `#graphql
type User {
id: ID!
name: String!
email: String!
posts(limit: Int, offset: Int): [Post!]!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
}
type Query {
user(id: ID!): User
users: [User!]!
}
type Mutation {
createUser(name: String!, email: String!): User!
}
`;
const resolvers = {
Query: {
user: async (_, { id }) => {
return db.user.findUnique({ where: { id } });
},
users: async () => {
return db.user.findMany();
}
},
Mutation: {
createUser: async (_, { name, email }) => {
return db.user.create({ data: { name, email } });
}
},
// Field-level resolvers
User: {
posts: async (parent, { limit = 10, offset = 0 }) => {
return db.post.findMany({
where: { authorId: parent.id },
take: limit,
skip: offset
});
}
}
};
const server = new ApolloServer({ typeDefs, resolvers });
Caching Strategies
REST: Using HTTP Cache
// REST: Standard HTTP caching
app.get('/users/:id', async (req, res) => {
const user = await getUser(req.params.id);
res
.set('Cache-Control', 'public, max-age=300') // 5-minute cache
.set('ETag', `"${user.version}"`)
.json(user);
});
// Client side
fetch('/users/123', {
headers: {
'If-None-Match': '"v1"' // Conditional request
}
});
// → 304 Not Modified (cache valid)
GraphQL: Client-Side Caching
// GraphQL: Apollo Client normalized cache
import { ApolloClient, InMemoryCache } from '@apollo/client';
const client = new ApolloClient({
uri: '/graphql',
cache: new InMemoryCache({
typePolicies: {
User: {
keyFields: ['id'], // Cache key
},
Post: {
keyFields: ['id'],
}
}
})
});
// Cache is automatically normalized
// User:123 → { name: "...", email: "..." }
// Post:456 → { title: "...", author: { __ref: "User:123" } }
Error Handling
REST: HTTP Status Codes
// REST: Standard HTTP status codes
app.get('/users/:id', async (req, res) => {
try {
const user = await getUser(req.params.id);
if (!user) {
return res.status(404).json({
error: 'NOT_FOUND',
message: 'User not found'
});
}
res.json(user);
} catch (error) {
res.status(500).json({
error: 'INTERNAL_ERROR',
message: 'A server error occurred'
});
}
});
// Response example
// HTTP 404
{
"error": "NOT_FOUND",
"message": "User not found"
}
GraphQL: errors Array
// GraphQL: Partial errors are possible
const resolvers = {
Query: {
user: async (_, { id }) => {
const user = await getUser(id);
if (!user) {
throw new GraphQLError('User not found', {
extensions: {
code: 'NOT_FOUND',
argumentName: 'id'
}
});
}
return user;
}
}
};
// Response example (partial success)
{
"data": {
"user": null,
"posts": [...] // Other fields succeeded
},
"errors": [
{
"message": "User not found",
"path": ["user"],
"extensions": {
"code": "NOT_FOUND"
}
}
]
}
Performance Characteristics
Comparison Table
| Aspect | REST | GraphQL |
|---|---|---|
| Number of requests | Tends to increase | Can be minimized |
| Payload size | Over-fetching | Optimized |
| HTTP caching | Native support | Requires additional implementation |
| CDN caching | Easy | Requires workarounds |
| N+1 problem (server) | None | Address with DataLoader |
GraphQL N+1 Problem Solutions
// Optimization with DataLoader
import DataLoader from 'dataloader';
// Define batch loader
const userLoader = new DataLoader(async (userIds: string[]) => {
const users = await db.user.findMany({
where: { id: { in: userIds } }
});
// Return preserving ID order
const userMap = new Map(users.map(u => [u.id, u]));
return userIds.map(id => userMap.get(id));
});
// Use in resolver
const resolvers = {
Post: {
author: (parent, _, context) => {
return context.userLoader.load(parent.authorId);
}
}
};
// Result: Batch query instead of individual queries
// SELECT * FROM users WHERE id IN ('1', '2', '3', ...)
Security Considerations
REST: Per-Endpoint Control
// REST: Authorization per route
app.get('/admin/users', requireAdmin, async (req, res) => {
// Admin access only
});
app.get('/users/:id', async (req, res) => {
// Public endpoint
});
GraphQL: Field-Level Control
// GraphQL: Control with directives
const typeDefs = `#graphql
directive @auth(requires: Role!) on FIELD_DEFINITION
type User {
id: ID!
name: String!
email: String! @auth(requires: OWNER)
salary: Int @auth(requires: ADMIN)
}
`;
// Query complexity limiting
import { createComplexityLimitRule } from 'graphql-validation-complexity';
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
createComplexityLimitRule(1000) // Complexity limit
]
});
GraphQL-Specific Security Measures
// 1. Query depth limiting
import depthLimit from 'graphql-depth-limit';
const server = new ApolloServer({
validationRules: [depthLimit(10)] // Maximum depth 10
});
// 2. Disable introspection (production)
const server = new ApolloServer({
introspection: process.env.NODE_ENV !== 'production'
});
// 3. Persisted queries
const server = new ApolloServer({
persistedQueries: {
cache: new RedisCache()
}
});
Selection Criteria
Cases Where REST is Suitable
Situations to choose REST:
- ✓ Simple CRUD operations are the focus
- ✓ Want to maximize HTTP caching
- ✓ CDN/edge caching is important
- ✓ Team has extensive REST experience
- ✓ Providing as a public API
- ✓ Many file uploads/downloads
Cases Where GraphQL is Suitable
Situations to choose GraphQL:
- ✓ Complex data requirements (many relationships)
- ✓ Multiple clients (Web, Mobile, etc.)
- ✓ Client-driven development (frontend priority)
- ✓ Real-time features (Subscriptions)
- ✓ Microservices BFF (Backend for Frontend)
- ✓ Bandwidth optimization is important (mobile)
Hybrid Approach
// Pattern using both
const app = express();
// REST: File upload
app.post('/upload', upload.single('file'), (req, res) => {
res.json({ url: req.file.path });
});
// REST: Health check
app.get('/health', (req, res) => {
res.json({ status: 'ok' });
});
// GraphQL: Data fetching/updating
app.use('/graphql', apolloMiddleware);
Summary
GraphQL and REST are API design paradigms with different strengths.
REST Strengths
- Maximizes HTTP functionality
- Simple and easy to understand
- Rich tool ecosystem
- Easy caching
GraphQL Strengths
- Flexible data fetching
- Type-safe schema
- Complex data fetching in a single request
- Improved developer experience
Selection Guidelines
- Analyze requirements: Data complexity, client diversity
- Team skills: Existing experience and learning cost
- Performance requirements: Caching, bandwidth, latency
- Future extensibility: Scalability, maintainability
Whichever you choose, you can build excellent APIs with proper design and implementation. What’s important is making the choice that fits your project’s characteristics.
References
- GraphQL Official Documentation
- REST API Design Guidelines
- Apollo Server Documentation
- OpenAPI Specification