← Back to home
GraphQL vs REST: Choosing the Right API Architecture
Compare GraphQL and REST APIs to make informed decisions for your next project
graphqlrestapibackendarchitecture
GraphQL vs REST: Choosing the Right API Architecture
When building APIs, choosing between GraphQL and REST is a crucial architectural decision. Let's explore both approaches to help you make the right choice.
REST API Fundamentals
REST (Representational State Transfer) follows standard HTTP methods and URL patterns:
// REST API endpoints
GET /api/users // Get all users
GET /api/users/123 // Get specific user
POST /api/users // Create user
PUT /api/users/123 // Update user
DELETE /api/users/123 // Delete user
// Example REST implementation (Express.js)
const express = require('express');
const app = express();
// Get all users
app.get('/api/users', async (req, res) => {
const users = await User.findAll();
res.json(users);
});
// Get user by ID
app.get('/api/users/:id', async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
});
// Create user
app.post('/api/users', async (req, res) => {
const user = await User.create(req.body);
res.status(201).json(user);
});
GraphQL Fundamentals
GraphQL provides a query language and runtime for APIs:
# GraphQL Schema
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
profile: Profile
}
type Post {
id: ID!
title: String!
content: String!
author: User!
createdAt: String!
}
type Query {
users: [User!]!
user(id: ID!): User
posts: [Post!]!
}
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
deleteUser(id: ID!): Boolean!
}
// GraphQL resolvers
const resolvers = {
Query: {
users: () => User.findAll(),
user: (_, { id }) => User.findById(id),
posts: () => Post.findAll()
},
Mutation: {
createUser: (_, { input }) => User.create(input),
updateUser: (_, { id, input }) => User.update(id, input),
deleteUser: (_, { id }) => User.delete(id)
},
User: {
posts: (user) => Post.findByUserId(user.id),
profile: (user) => Profile.findByUserId(user.id)
}
};
// Apollo Server setup
const { ApolloServer } = require('apollo-server-express');
const server = new ApolloServer({
typeDefs,
resolvers
});
server.applyMiddleware({ app });
Data Fetching Comparison
REST: Multiple Requests
// REST client - multiple requests needed
const fetchUserData = async (userId) => {
// Request 1: Get user
const userResponse = await fetch(`/api/users/${userId}`);
const user = await userResponse.json();
// Request 2: Get user's posts
const postsResponse = await fetch(`/api/users/${userId}/posts`);
const posts = await postsResponse.json();
// Request 3: Get user's profile
const profileResponse = await fetch(`/api/users/${userId}/profile`);
const profile = await profileResponse.json();
return { user, posts, profile };
};
// Over-fetching: Getting more data than needed
const users = await fetch('/api/users'); // Returns all user fields
// But we only need name and email for a list view
GraphQL: Single Request
# GraphQL query - single request with exact data needed
query GetUserData($userId: ID!) {
user(id: $userId) {
id
name
email
posts {
id
title
createdAt
}
profile {
bio
avatar
}
}
}
// GraphQL client
const { request, gql } = require('graphql-request');
const GET_USER_DATA = gql`
query GetUserData($userId: ID!) {
user(id: $userId) {
id
name
email
posts {
id
title
createdAt
}
profile {
bio
avatar
}
}
}
`;
const userData = await request('/graphql', GET_USER_DATA, { userId: '123' });
Caching Strategies
REST Caching
// HTTP caching with REST
app.get('/api/users/:id', (req, res) => {
res.set({
'Cache-Control': 'public, max-age=300', // 5 minutes
'ETag': generateETag(user)
});
res.json(user);
});
// Redis caching
const cache = require('redis').createClient();
app.get('/api/users/:id', async (req, res) => {
const cacheKey = `user:${req.params.id}`;
// Check cache first
const cached = await cache.get(cacheKey);
if (cached) {
return res.json(JSON.parse(cached));
}
// Fetch from database
const user = await User.findById(req.params.id);
// Cache for 5 minutes
await cache.setex(cacheKey, 300, JSON.stringify(user));
res.json(user);
});
GraphQL Caching
// Apollo Server with caching
const { ApolloServer } = require('apollo-server-express');
const { RedisCache } = require('apollo-server-cache-redis');
const server = new ApolloServer({
typeDefs,
resolvers,
cache: new RedisCache({
host: 'localhost',
port: 6379,
}),
cacheControl: {
defaultMaxAge: 300, // 5 minutes
}
});
// Field-level caching
const resolvers = {
Query: {
user: async (_, { id }, { dataSources }) => {
return dataSources.userAPI.getUserById(id);
}
},
User: {
posts: async (user, _, { dataSources }) => {
// Cache posts separately
return dataSources.postAPI.getPostsByUserId(user.id);
}
}
};
// Automatic Persisted Queries (APQ)
const server = new ApolloServer({
typeDefs,
resolvers,
persistedQueries: {
cache: new RedisCache()
}
});
Error Handling
REST Error Handling
// REST error responses
app.get('/api/users/:id', async (req, res) => {
try {
const user = await User.findById(req.params.id);
if (!user) {
return res.status(404).json({
error: 'User not found',
code: 'USER_NOT_FOUND'
});
}
res.json(user);
} catch (error) {
res.status(500).json({
error: 'Internal server error',
code: 'INTERNAL_ERROR'
});
}
});
// Client error handling
const response = await fetch('/api/users/123');
if (!response.ok) {
if (response.status === 404) {
console.log('User not found');
} else if (response.status === 500) {
console.log('Server error');
}
return;
}
const user = await response.json();
GraphQL Error Handling
// GraphQL error handling
const resolvers = {
Query: {
user: async (_, { id }) => {
const user = await User.findById(id);
if (!user) {
throw new UserInputError('User not found', {
code: 'USER_NOT_FOUND',
userId: id
});
}
return user;
}
}
};
// Custom error class
class UserInputError extends Error {
constructor(message, properties) {
super(message);
this.extensions = {
code: 'USER_INPUT_ERROR',
...properties
};
}
}
// Client error handling
const { data, errors } = await client.query({
query: GET_USER,
variables: { id: '123' },
errorPolicy: 'all' // Return partial data with errors
});
if (errors) {
errors.forEach(error => {
if (error.extensions.code === 'USER_NOT_FOUND') {
console.log('User not found');
}
});
}
Real-time Features
REST with Server-Sent Events
// Server-Sent Events for real-time updates
app.get('/api/users/:id/events', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
});
const userId = req.params.id;
// Listen for user updates
const listener = (data) => {
res.write(`data: ${JSON.stringify(data)}\n\n`);
};
userUpdateEmitter.on(userId, listener);
req.on('close', () => {
userUpdateEmitter.off(userId, listener);
});
});
// Client
const eventSource = new EventSource('/api/users/123/events');
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
updateUI(data);
};
GraphQL Subscriptions
# GraphQL subscriptions
type Subscription {
userUpdated(id: ID!): User!
postAdded: Post!
messageAdded(chatId: ID!): Message!
}
// GraphQL subscription resolver
const { PubSub } = require('apollo-server-express');
const pubsub = new PubSub();
const resolvers = {
Subscription: {
userUpdated: {
subscribe: (_, { id }) => pubsub.asyncIterator(`USER_UPDATED_${id}`)
},
postAdded: {
subscribe: () => pubsub.asyncIterator('POST_ADDED')
}
},
Mutation: {
updateUser: async (_, { id, input }) => {
const user = await User.update(id, input);
// Publish update
pubsub.publish(`USER_UPDATED_${id}`, { userUpdated: user });
return user;
}
}
};
// Client subscription
const subscription = gql`
subscription UserUpdated($id: ID!) {
userUpdated(id: $id) {
id
name
email
}
}
`;
client.subscribe({
query: subscription,
variables: { id: '123' }
}).subscribe({
next: (data) => updateUI(data.userUpdated)
});
When to Choose REST
✅ Choose REST when:
- Building simple CRUD applications
- Team is familiar with REST conventions
- Caching requirements are straightforward
- Working with existing REST infrastructure
- Building public APIs for third-party integration
- Need predictable URL patterns for SEO
// Example: Simple blog API
GET /api/posts // List posts
GET /api/posts/my-post // Get specific post
POST /api/posts // Create post
PUT /api/posts/my-post // Update post
DELETE /api/posts/my-post // Delete post
When to Choose GraphQL
✅ Choose GraphQL when:
- Frontend needs vary significantly
- Mobile applications require data efficiency
- Multiple clients consume the same API
- Real-time features are important
- Team can handle the additional complexity
- Rapid frontend development is priority
# Example: Complex social media query
query GetFeed($userId: ID!, $limit: Int!) {
user(id: $userId) {
feed(limit: $limit) {
id
content
author {
name
avatar
}
likes {
count
byCurrentUser
}
comments(limit: 3) {
content
author {
name
}
}
}
}
}
Conclusion
Both REST and GraphQL have their strengths:
- REST is mature, simple, and well-understood with excellent caching
- GraphQL offers flexibility, efficiency, and powerful real-time features
Consider your team's expertise, project requirements, and long-term maintenance when making the choice. Many successful applications use both: REST for simple operations and GraphQL for complex data requirements.