GraphQL vs REST: Complete Comparison
A comprehensive comparison of GraphQL and REST, helping you choose the right API architecture for your project.
GraphQL vs REST: The Complete Comparison Guide
Choosing between GraphQL and REST is one of the most important architectural decisions you'll make when building an API. Both have strengths and weaknesses, and the right choice depends on your specific use case.
This comprehensive guide provides detailed comparisons, practical examples, and decision criteria to help you make an informed choice.
What are GraphQL and REST?
GraphQL
GraphQL is a query language for APIs that allows clients to request exactly the data they need, no more and no less. It was developed by Facebook in 2012 and open-sourced in 2015.
Key Characteristics:
- Schema-based
- Strongly typed
- Single endpoint
- Client-driven queries
- Real-time subscriptions
- Introspection
REST
REST (Representational State Transfer) is an architectural style for designing networked applications. It uses standard HTTP methods and resource-based URLs.
Key Characteristics:
- Resource-oriented
- Stateless
- HTTP semantics
- Multiple endpoints
- Server-driven responses
- Caching-friendly
Request Structure Comparison
GraphQL Example
# Query: Fetch user with specific fields
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
posts {
id
title
createdAt
}
}
}
# Variables
{
"id": "123"
}
# Response (single request)
{
"data": {
"user": {
"id": "123",
"name": "John Doe",
"email": "john@example.com",
"posts": [
{
"id": "1",
"title": "First Post",
"createdAt": "2025-01-01T00:00:00Z"
}
]
}
}
}
REST Example
# Multiple requests needed
# 1. Fetch user
GET /api/users/123
# 2. Fetch user posts
GET /api/users/123/posts
# Response 1
{
"id": "123",
"name": "John Doe",
"email": "john@example.com"
}
# Response 2
{
"posts": [
{
"id": "1",
"title": "First Post",
"createdAt": "2025-01-01T00:00:00Z"
}
]
}
Key Differences at a Glance
| Feature | GraphQL | REST |
|---|---|---|
| Endpoints | Single | Multiple |
| Data Fetching | Client specifies | Server specifies |
| Over-fetching | No | Common |
| Under-fetching | No | Common |
| Versioning | No need | Required |
| Real-time | Native subscriptions | WebSockets/SSE |
| Caching | Complex | Built-in HTTP |
| Error Handling | Partial errors | HTTP status codes |
| Learning Curve | Steeper | Shallower |
| Tooling | Strong | Mature |
| Community | Growing | Large |
When to Use Each
Use GraphQL When:
-
Complex Data Requirements
- Multiple related resources needed
- Varying data requirements per screen
- Nested data relationships
-
Mobile Applications
- Limited bandwidth
- Need precise data control
- Offline-first architecture
-
Real-Time Features
- Live updates
- Subscriptions
- Collaborative apps
-
Rapid Development
- Frequent schema changes
- Evolving requirements
- Multiple client teams
Use REST When:
-
Simple CRUD Operations
- Straightforward resource management
- Standard create, read, update, delete
- Predictable data access patterns
-
Public APIs
- Third-party integrations
- Need simple access
- Caching critical
-
Resource-Based Applications
- Clear resource boundaries
- Simple relationships
- Stateless operations
-
Existing Infrastructure
- REST-compatible systems
- HTTP caching infrastructure
- CDNs and proxies
Performance Considerations
Data Over-fetching
// GraphQL: Fetch only what's needed
const GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) {
id
name // Only name needed
}
}
`
// Response: 100 bytes
{
"data": {
"user": {
"id": "123",
"name": "John Doe"
}
}
}
// REST: Fetches entire resource
GET /api/users/123
// Response: 500 bytes (includes email, posts, etc.)
{
"id": "123",
"name": "John Doe",
"email": "john@example.com",
"posts": [/* ... */]
}
Network Requests
// GraphQL: Single request
const data = await client.query({
query: GET_USER_AND_POSTS
})
// REST: Multiple requests (n+1 problem)
const user = await fetch('/api/users/123')
const posts = await fetch('/api/users/123/posts')
const comments = await fetch('/api/posts/1/comments')
// ... more requests
Caching
// REST: Built-in HTTP caching
// Can use ETag, Last-Modified, Cache-Control headers
const cachedResponse = await fetch('/api/users/123', {
cache: 'force-cache' // Browser handles caching
})
// GraphQL: Requires custom caching
// Need to implement query-based caching
const queryCache = new Map()
async function cachedQuery(query) {
const cacheKey = JSON.stringify(query)
if (queryCache.has(cacheKey)) {
return queryCache.get(cacheKey)
}
const result = await client.query({ query })
queryCache.set(cacheKey, result)
return result
}
Implementation Examples
GraphQL Server
// server.js
import { ApolloServer } from '@apollo/server'
import { startStandaloneServer } from '@apollo/server/standalone'
import { resolvers, typeDefs } from './schema'
const server = new ApolloServer({
typeDefs,
resolvers,
})
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
})
console.log(`🚀 Server ready at ${url}`)
// schema.js
import { gql } from 'apollo-server'
export const typeDefs = gql`
type Query {
user(id: ID!): User
users: [User!]!
posts: [Post!]!
}
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
deleteUser(id: ID!): Boolean!
}
type Subscription {
userUpdated(id: ID!): User!
}
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
createdAt: DateTime!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
createdAt: DateTime!
}
input CreateUserInput {
name: String!
email: String!
}
input UpdateUserInput {
name: String
email: String
}
scalar DateTime
`
// resolvers.js
import { PubSub } from 'graphql-subscriptions'
const pubsub = new PubSub()
const USER_UPDATED = 'USER_UPDATED'
// Mock database
const users = new Map()
const posts = new Map()
export const resolvers = {
Query: {
user: (_parent, { id }) => users.get(id),
users: () => Array.from(users.values()),
posts: () => Array.from(posts.values()),
},
Mutation: {
createUser: (_parent, { input }) => {
const user = {
id: String(users.size + 1),
...input,
createdAt: new Date().toISOString(),
}
users.set(user.id, user)
return user
},
updateUser: (_parent, { id, input }) => {
const user = users.get(id)
if (!user) throw new Error('User not found')
const updated = { ...user, ...input }
users.set(id, updated)
// Publish update
pubsub.publish(USER_UPDATED, { userUpdated: updated })
return updated
},
deleteUser: (_parent, { id }) => {
const existed = users.delete(id)
if (!existed) throw new Error('User not found')
return true
},
},
Subscription: {
userUpdated: {
subscribe: (_parent, { id }) => {
const filter = (payload) => payload.userUpdated.id === id
return pubsub.asyncIterableIterator(USER_UPDATED, filter)
},
},
},
}
REST Server
// server.js
import express from 'express'
import bodyParser from 'body-parser'
const app = express()
app.use(bodyParser.json())
// Mock database
const users = new Map()
let nextId = 1
// GET all users
app.get('/api/users', (req, res) => {
const { page = 1, limit = 10 } = req.query
const allUsers = Array.from(users.values())
const start = (page - 1) * limit
const end = start + parseInt(limit)
const paginated = allUsers.slice(start, end)
res.json({
data: paginated,
meta: {
page: parseInt(page),
limit: parseInt(limit),
total: allUsers.length,
totalPages: Math.ceil(allUsers.length / limit),
},
})
})
// GET single user
app.get('/api/users/:id', (req, res) => {
const user = users.get(req.params.id)
if (!user) {
return res.status(404).json({ error: 'User not found' })
}
res.json(user)
})
// POST create user
app.post('/api/users', (req, res) => {
const user = {
id: String(nextId++),
...req.body,
createdAt: new Date().toISOString(),
}
users.set(user.id, user)
res.status(201).json(user)
})
// PUT update user
app.put('/api/users/:id', (req, res) => {
const existing = users.get(req.params.id)
if (!existing) {
return res.status(404).json({ error: 'User not found' })
}
const updated = { ...existing, ...req.body }
users.set(req.params.id, updated)
res.json(updated)
})
// DELETE user
app.delete('/api/users/:id', (req, res) => {
const existed = users.delete(req.params.id)
if (!existed) {
return res.status(404).json({ error: 'User not found' })
}
res.status(204).send()
})
// Start server
app.listen(3000, () => {
console.log('🚀 REST server running on port 3000')
})
Client Implementation
GraphQL Client (Apollo)
// client.js
import { ApolloClient, InMemoryCache, createHttpLink } from '@apollo/client'
import { gql } from '@apollo/client'
const httpLink = createHttpLink({
uri: 'http://localhost:4000/graphql',
})
const client = new ApolloClient({
link: httpLink,
cache: new InMemoryCache(),
})
// Query
export async function getUser(id) {
const GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
posts {
id
title
}
}
}
`
const { data } = await client.query({
query: GET_USER,
variables: { id },
})
return data.user
}
// Mutation
export async function updateUser(id, input) {
const UPDATE_USER = gql`
mutation UpdateUser($id: ID!, $input: UpdateUserInput!) {
updateUser(id: $id, input: $input) {
id
name
email
}
}
`
const { data } = await client.mutate({
mutation: UPDATE_USER,
variables: { id, input },
})
return data.updateUser
}
// Subscription
export function subscribeToUserUpdates(id, onUpdate) {
const USER_UPDATED = gql`
subscription UserUpdated($id: ID!) {
userUpdated(id: $id) {
id
name
email
}
}
`
const subscription = client.subscribe({
query: USER_UPDATED,
variables: { id },
})
subscription.subscribe({
next: ({ data }) => {
onUpdate(data.userUpdated)
},
error: (error) => {
console.error('Subscription error:', error)
},
})
return () => subscription.unsubscribe()
}
REST Client (Axios)
// client.js
import axios from 'axios'
const api = axios.create({
baseURL: 'http://localhost:3000/api',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
})
// GET
export async function getUsers(params = {}) {
const response = await api.get('/users', { params })
return response.data
}
export async function getUser(id) {
const response = await api.get(`/users/${id}`)
return response.data
}
// POST
export async function createUser(data) {
const response = await api.post('/users', data)
return response.data
}
// PUT
export async function updateUser(id, data) {
const response = await api.put(`/users/${id}`, data)
return response.data
}
// DELETE
export async function deleteUser(id) {
await api.delete(`/users/${id}`)
return true
}
// With real-time (polling)
export function subscribeToUserUpdates(id, onUpdate, interval = 5000) {
const poll = async () => {
try {
const user = await getUser(id)
onUpdate(user)
} catch (error) {
console.error('Polling error:', error)
}
}
const intervalId = setInterval(poll, interval)
// Initial fetch
poll()
return () => clearInterval(intervalId)
}
Advanced Features
GraphQL Advanced
Batching and Caching
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client'
const httpLink = new HttpLink({
uri: 'http://localhost:4000/graphql',
batchInterval: 10, // Batch requests within 10ms
})
const client = new ApolloClient({
link: httpLink,
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
user: {
// Cache user by ID
keyArgs: ['id'],
merge(existing, incoming) => ({
...existing,
...incoming,
}),
},
},
},
},
}),
})
Directives
# Custom directive for deprecation
directive @deprecated(reason: String) on FIELD_DEFINITION
type Query {
user(id: ID!): User @deprecated(reason: "Use getUser instead")
getUser(id: ID!): User
}
REST Advanced
HATEOAS (Hypermedia as the Engine of Application State)
app.get('/api/users/:id', (req, res) => {
const user = users.get(req.params.id)
const response = {
...user,
_links: {
self: `/api/users/${user.id}`,
posts: `/api/users/${user.id}/posts`,
update: `/api/users/${user.id}`,
},
}
res.json(response)
})
Versioning
// Versioned routes
app.use('/api/v1', v1Router)
app.use('/api/v2', v2Router)
// Or via header
app.use((req, res, next) => {
const version = req.headers['api-version'] || 'v1'
if (version === 'v2') {
req.version = 'v2'
return v2Router(req, res, next)
}
req.version = 'v1'
v1Router(req, res, next)
})
Security Considerations
GraphQL Security
// Rate limiting per query
const rateLimiter = new Map()
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
// Limit query complexity
queryComplexity({
maximumComplexity: 1000,
onComplete: (complexity) => {
console.log(`Query complexity: ${complexity}`)
},
}),
// Limit query depth
depthLimit({
maxDepth: 5,
}),
],
context: async ({ req }) => {
// Rate limit
const ip = req.ip
const key = `${ip}:${Date.now() / 60000}` // 1-minute window
if (!rateLimiter.has(key)) {
rateLimiter.set(key, 0)
}
rateLimiter.set(key, rateLimiter.get(key) + 1)
if (rateLimiter.get(key) > 100) {
throw new Error('Rate limit exceeded')
}
// Authentication
const token = req.headers.authorization
const user = await authenticate(token)
return { user }
},
})
REST Security
// Rate limiting middleware
const rateLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 100, // limit each IP to 100 requests per windowMs
})
app.use('/api/', rateLimiter)
// CORS
app.use(cors({
origin: 'https://yourdomain.com',
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
}))
// Helmet for security headers
app.use(helmet())
// Input validation
app.post('/api/users', (req, res) => {
const { error } = validateUser(req.body)
if (error) {
return res.status(400).json({ error })
}
// ... create user
})
Testing
GraphQL Testing
// user.test.js
import { gql } from '@apollo/server'
import { resolvers, typeDefs } from './schema'
describe('User queries', () => {
const testServer = new ApolloServer({
typeDefs,
resolvers,
})
it('should fetch user by ID', async () => {
const GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
`
const result = await testServer.executeOperation({
query: GET_USER,
variableValues: { id: '1' },
})
expect(result.errors).toBeUndefined()
expect(result.data.user).toMatchObject({
id: '1',
name: 'Test User',
email: 'test@example.com',
})
})
it('should return null for non-existent user', async () => {
const GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) {
id
}
}
`
const result = await testServer.executeOperation({
query: GET_USER,
variableValues: { id: '999' },
})
expect(result.data.user).toBeNull()
})
})
REST Testing
// users.test.js
import request from 'supertest'
import app from './server'
describe('Users API', () => {
describe('GET /api/users', () => {
it('should return all users', async () => {
const response = await request(app)
.get('/api/users')
.expect(200)
expect(response.body.data).toBeInstanceOf(Array)
expect(response.body.meta.total).toBeDefined()
})
it('should support pagination', async () => {
const response = await request(app)
.get('/api/users?page=1&limit=10')
.expect(200)
expect(response.body.data.length).toBeLessThanOrEqual(10)
expect(response.body.meta.page).toBe(1)
})
})
describe('GET /api/users/:id', () => {
it('should return user by ID', async () => {
const response = await request(app)
.get('/api/users/1')
.expect(200)
expect(response.body.id).toBe('1')
})
it('should return 404 for non-existent user', async () => {
await request(app)
.get('/api/users/999')
.expect(404)
})
})
describe('POST /api/users', () => {
it('should create new user', async () => {
const newUser = {
name: 'New User',
email: 'new@example.com',
}
const response = await request(app)
.post('/api/users')
.send(newUser)
.expect(201)
expect(response.body.id).toBeDefined()
expect(response.body.name).toBe(newUser.name)
expect(response.body.email).toBe(newUser.email)
})
it('should validate input', async () => {
const invalidUser = {
name: '', // Invalid
}
await request(app)
.post('/api/users')
.send(invalidUser)
.expect(400)
})
})
})
Hybrid Approaches
GraphQL over REST
// Use GraphQL to orchestrate REST APIs
import { ApolloServer, RESTDataSource } from '@apollo/datasource-rest'
class UserAPI extends RESTDataSource {
constructor() {
super()
this.baseURL = 'http://localhost:3000/api'
}
async getUser(id) {
return this.get(`/users/${id}`)
}
async getUsers() {
return this.get('/users')
}
}
const resolvers = {
Query: {
user: async (_parent, { id }, { dataSources }) => {
return dataSources.userAPI.getUser(id)
},
users: async (_parent, _args, { dataSources }) => {
return dataSources.userAPI.getUsers()
},
},
}
const server = new ApolloServer({
typeDefs,
resolvers,
dataSources: () => ({ userAPI: new UserAPI() }),
})
REST Gateway for GraphQL
// GraphQL service acting as gateway
const typeDefs = gql`
type User {
id: ID!
name: String!
email: String!
}
type Query {
user(id: ID!): User
}
`
const resolvers = {
Query: {
user: async (_parent, { id }) => {
// Call existing REST API
const response = await fetch(`http://localhost:3000/api/users/${id}`)
return response.json()
},
},
}
Migration Strategies
From REST to GraphQL
// Phase 1: Introduce GraphQL alongside REST
// Keep REST endpoints functional
// Add GraphQL endpoint at /graphql
// Phase 2: Migrate clients to GraphQL
// Update one client at a time
// Use feature flags
// Phase 3: Deprecate REST endpoints
// Add deprecation warnings
// Document sunset timeline
// Phase 4: Remove REST endpoints
// After client migration complete
// Clean up unused code
From GraphQL to REST
// Phase 1: Design REST endpoints
// Identify key GraphQL operations
// Design corresponding REST resources
// Phase 2: Implement REST endpoints
// Keep GraphQL functional
// Use same business logic
// Phase 3: Migrate clients
// Update clients to use REST
// Test thoroughly
// Phase 4: Remove GraphQL
// After migration complete
// Remove GraphQL dependencies
Decision Framework
Use GraphQL If:
- Multiple different clients with varying data needs
- Complex nested data relationships
- Need real-time updates
- Rapidly evolving schema
- Bandwidth-constrained clients (mobile)
- Multiple microservices to aggregate
Use REST If:
- Simple CRUD operations
- Public API
- Heavy caching requirements
- Existing REST infrastructure
- Simple data models
- Limited development resources
Conclusion
GraphQL and REST are both powerful approaches, each with distinct advantages. GraphQL excels at flexibility and efficiency, while REST offers simplicity and maturity. The right choice depends on your specific requirements, team expertise, and project constraints.
Many successful companies use both: GraphQL for complex internal applications and REST for public APIs. Consider your use case carefully before committing to either approach.
Key Takeaways
- GraphQL for flexibility, efficiency, real-time
- REST for simplicity, caching, public APIs
- Hybrid approaches can leverage both
- Consider data requirements, client types, team expertise
- Test thoroughly before committing
- Plan migration strategy if changing approach
- Monitor performance and iterate
Next Steps
- Analyze your API requirements
- Prototype both approaches
- Evaluate with your team
- Choose based on use case
- Plan implementation roadmap
- Test thoroughly
- Monitor and iterate
Both GraphQL and REST have proven themselves in production. Choose wisely, implement carefully, and deliver excellent APIs.
Ready to choose between GraphQL and REST? Evaluate your requirements, prototype both, and select the approach that best fits your needs.