Skip to main content
โšก Calmops

REST vs GraphQL: A Comprehensive Comparison and Decision Framework

This comprehensive guide compares REST and GraphQL API paradigms, exploring their strengths, weaknesses, and optimal use cases to help you make informed architectural decisions.

Overview

What is REST?

REST (Representational State Transfer) is an architectural style that uses HTTP methods to perform CRUD operations on resources identified by URLs.

GET    /api/users          # Get all users
GET    /api/users/123      # Get user 123
POST   /api/users          # Create new user
PUT    /api/users/123      # Update user 123
DELETE /api/users/123      # Delete user 123

What is GraphQL?

GraphQL is a query language for APIs that allows clients to request exactly the data they need.

query {
  user(id: "123") {
    name
    email
    posts {
      title
      createdAt
    }
  }
}

Architectural Comparison

REST Architecture

graph TD
    Client1[Client] -->|GET /users| API1[API Gateway]
    Client2[Client] -->|GET /users/123/posts| API1
    Client3[Client] -->|POST /orders| API1
    
    API1 -->|Route| Auth[Authentication]
    API1 -->|Route| Rate[Rate Limiter]
    
    Auth --> Users[Users Service]
    Auth --> Posts[Posts Service]
    Auth --> Orders[Orders Service]
    
    Users --> DB1[(User DB)]
    Posts --> DB2[(Post DB)]
    Orders --> DB3[(Order DB)]
    
    Users --> Cache[(Redis Cache)]

GraphQL Architecture

graph TD
    Client[Client] -->|Query| API[GraphQL API]
    Client -->|Mutation| API
    Client -->|Subscription| API
    
    API -->|Parse| Parser[Parser]
    Parser -->|Validate| Validator[Validator]
    Validator -->|Execute| Resolver[Resolver]
    
    Resolver -->|Fetch| Users[Users Service]
    Resolver -->|Fetch| Posts[Posts Service]
    Resolver -->|Fetch| Orders[Orders Service]
    
    Users --> DB1[(User DB)]
    Posts --> DB2[(Post DB)]
    Orders --> DB3[(Order DB)]

Data Fetching Patterns

REST: Multiple Endpoints

// REST: Fetching nested data requires multiple requests
// Request 1: Get user
const userResponse = await fetch('/api/users/123');
const user = await userResponse.json();
// { id: 123, name: "John", email: "[email protected]" }

// Request 2: Get user's posts
const postsResponse = await fetch('/api/users/123/posts');
const posts = await postsResponse.json();
// [{ id: 1, title: "Post 1" }, { id: 2, title: "Post 2" }]

// Request 3: Get posts' comments
const commentsResponse = await fetch('/api/posts/1/comments');
const comments = await commentsResponse.json();
// [{ id: 1, text: "Comment 1" }]

// Problem: Over-fetching - we get more data than needed
// Each endpoint returns fixed structure

GraphQL: Single Request

// GraphQL: Single request with exact fields
const query = `
  query GetUserData($userId: ID!) {
    user(id: $userId) {
      name
      email
      posts(first: 5) {
        title
        comments(first: 3) {
          text
          author {
            name
          }
        }
      }
    }
  }
`;

const response = await fetch('/graphql', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ query, variables: { userId: '123' } })
});

const { data } = await response.json();
// Returns exactly what was requested - no over-fetching

REST API Implementation

Express REST API

const express = require('express');
const cors = require('cors');
const helmet = require('helmet');

const app = express();
app.use(cors());
app.use(helmet());
app.use(express.json());

// In-memory data store (replace with real DB)
const db = {
  users: [
    { id: 1, name: 'John', email: '[email protected]', role: 'admin' },
    { id: 2, name: 'Jane', email: '[email protected]', role: 'user' }
  ],
  posts: [
    { id: 1, userId: 1, title: 'First Post', content: 'Hello World' },
    { id: 2, userId: 1, title: 'Second Post', content: 'Another post' }
  ],
  comments: [
    { id: 1, postId: 1, userId: 2, text: 'Great post!' }
  ]
};

// GET all resources
app.get('/api/users', (req, res) => {
  const { role, limit } = req.query;
  let users = [...db.users];
  
  if (role) {
    users = users.filter(u => u.role === role);
  }
  
  if (limit) {
    users = users.slice(0, parseInt(limit));
  }
  
  res.json(users);
});

// GET single resource
app.get('/api/users/:id', (req, res) => {
  const user = db.users.find(u => u.id === parseInt(req.params.id));
  
  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }
  
  res.json(user);
});

// GET nested resource
app.get('/api/users/:id/posts', (req, res) => {
  const userId = parseInt(req.params.id);
  const posts = db.posts.filter(p => p.userId === userId);
  res.json(posts);
});

// GET deeply nested
app.get('/api/users/:id/posts/:postId/comments', (req, res) => {
  const userId = parseInt(req.params.id);
  const postId = parseInt(req.params.postId);
  
  const comments = db.comments.filter(
    c => c.postId === postId
  );
  res.json(comments);
});

// POST create resource
app.post('/api/users', (req, res) => {
  const { name, email, role = 'user' } = req.body;
  
  if (!name || !email) {
    return res.status(400).json({ error: 'Name and email required' });
  }
  
  const newUser = {
    id: db.users.length + 1,
    name,
    email,
    role
  };
  
  db.users.push(newUser);
  res.status(201).json(newUser);
});

// PUT update resource
app.put('/api/users/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const userIndex = db.users.findIndex(u => u.id === id);
  
  if (userIndex === -1) {
    return res.status(404).json({ error: 'User not found' });
  }
  
  const updatedUser = {
    ...db.users[userIndex],
    ...req.body,
    id // Prevent ID change
  };
  
  db.users[userIndex] = updatedUser;
  res.json(updatedUser);
});

// DELETE resource
app.delete('/api/users/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const userIndex = db.users.findIndex(u => u.id === id);
  
  if (userIndex === -1) {
    return res.status(404).json({ error: 'User not found' });
  }
  
  db.users.splice(userIndex, 1);
  res.status(204).send();
});

app.listen(3000, () => console.log('REST API running on port 3000'));

GraphQL API Implementation

Apollo Server Setup

const { ApolloServer, gql } = require('apollo-server-express');
const express = require('express');
const { makeExecutableSchema } = require('@graphql-tools/schema');

// Type definitions
const typeDefs = gql`
  type User {
    id: ID!
    name: String!
    email: String!
    role: String!
    posts: [Post!]!
    friends: [User!]!
  }

  type Post {
    id: ID!
    title: String!
    content: String!
    author: User!
    comments: [Comment!]!
    createdAt: String!
  }

  type Comment {
    id: ID!
    text: String!
    author: User!
    post: Post!
  }

  input CreateUserInput {
    name: String!
    email: String!
    role: String = "user"
  }

  input UpdateUserInput {
    name: String
    email: String
    role: String
  }

  type Query {
    users(limit: Int, role: String): [User!]!
    user(id: ID!): User
    post(id: ID!): Post
    posts(limit: Int): [Post!]!
  }

  type Mutation {
    createUser(input: CreateUserInput!): User!
    updateUser(id: ID!, input: UpdateUserInput!): User
    deleteUser(id: ID!): Boolean!
    createPost(title: String!, content: String!, authorId: ID!): Post!
  }

  type Subscription {
    newPost: Post!
    newComment(postId: ID!): Comment!
  }
`;

// Sample data
let users = [
  { id: '1', name: 'John', email: '[email protected]', role: 'admin' },
  { id: '2', name: 'Jane', email: '[email protected]', role: 'user' }
];

let posts = [
  { id: '1', userId: '1', title: 'First Post', content: 'Hello World', createdAt: '2024-01-01' },
  { id: '2', userId: '1', title: 'Second Post', content: 'Another post', createdAt: '2024-01-02' }
];

let comments = [
  { id: '1', postId: '1', userId: '2', text: 'Great post!' }
];

// Resolvers
const resolvers = {
  Query: {
    users: (_, { limit, role }) => {
      let result = users;
      if (role) result = result.filter(u => u.role === role);
      if (limit) result = result.slice(0, limit);
      return result;
    },
    user: (_, { id }) => users.find(u => u.id === id),
    posts: (_, { limit }) => limit ? posts.slice(0, limit) : posts,
    post: (_, { id }) => posts.find(p => p.id === id)
  },
  
  User: {
    posts: (user) => posts.filter(p => p.userId === user.id),
    friends: (user) => {
      // Simple example: users with same role
      return users.filter(u => u.role === user.role && u.id !== user.id);
    }
  },
  
  Post: {
    author: (post) => users.find(u => u.id === post.userId),
    comments: (post) => comments.filter(c => c.postId === post.id)
  },
  
  Comment: {
    author: (comment) => users.find(u => u.id === comment.userId),
    post: (comment) => posts.find(p => p.id === comment.postId)
  },
  
  Mutation: {
    createUser: (_, { input }) => {
      const newUser = {
        id: String(users.length + 1),
        ...input
      };
      users.push(newUser);
      return newUser;
    },
    
    updateUser: (_, { id, input }) => {
      const index = users.findIndex(u => u.id === id);
      if (index === -1) return null;
      users[index] = { ...users[index], ...input };
      return users[index];
    },
    
    deleteUser: (_, { id }) => {
      const index = users.findIndex(u => u.id === id);
      if (index === -1) return false;
      users.splice(index, 1);
      return true;
    },
    
    createPost: (_, { title, content, authorId }) => {
      const newPost = {
        id: String(posts.length + 1),
        title,
        content,
        userId: authorId,
        createdAt: new Date().toISOString()
      };
      posts.push(newPost);
      return newPost;
    }
  }
};

// Create server
const schema = makeExecutableSchema({ typeDefs, resolvers });
const app = express();

const server = new ApolloServer({
  schema,
  context: ({ req }) => ({
    // Add authentication info
    user: req.user
  })
});

await server.start();
server.applyMiddleware({ app });

app.listen(4000, () => {
  console.log(`GraphQL server at http://localhost:4000${server.graphqlPath}`);
});

Performance Comparison

Over-fetching Problem

// REST: Get user profile - always returns all fields
// /api/users/123 might return 15 fields
// But mobile app only needs name and avatar (2 fields)
// Waste: 13 fields * millions of requests = significant bandwidth

// GraphQL: Request only what you need
const query = `
  query MobileUser($id: ID!) {
    user(id: $id) {
      name
      avatar
    }
  }
`;

Under-fetching Problem

// REST: Dashboard with multiple widgets
// Need 5 different API calls
const dashboardData = {
  user: await fetch('/api/user/123').then(r => r.json()),
  notifications: await fetch('/api/notifications').then(r => r.json()),
  recentPosts: await fetch('/api/users/123/posts?limit=5').then(r => r.json()),
  stats: await fetch('/api/stats').then(r => r.json()),
  messages: await fetch('/api/messages?unread=true').then(r => r.json())
};

// GraphQL: Single request
const query = `
  query Dashboard {
    user(id: "123") {
      name
      avatar
    }
    notifications(unread: true) {
      id
      message
    }
    posts(userId: "123", limit: 5) {
      title
      createdAt
    }
    stats {
      views
      likes
    }
    messages(unread: true) {
      id
      preview
    }
  }
`;

Caching Strategies

REST: HTTP Caching

// REST: HTTP Caching with ETag
app.get('/api/users/:id', (req, res) => {
  const user = getUser(req.params.id);
  
  // Generate ETag
  const etag = crypto.createHash('md5')
    .update(JSON.stringify(user))
    .digest('hex');
  
  // Check If-None-Match
  if (req.headers['if-none-match'] === etag) {
    return res.status(304).end(); // Not Modified
  }
  
  res.set('ETag', etag);
  res.set('Cache-Control', 'public, max-age=3600');
  res.json(user);
});

// Redis caching
app.get('/api/users/:id', async (req, res) => {
  const cacheKey = `user:${req.params.id}`;
  
  // Check cache
  const cached = await redis.get(cacheKey);
  if (cached) {
    return res.json(JSON.parse(cached));
  }
  
  const user = await db.users.findById(req.params.id);
  
  // Cache for 1 hour
  await redis.setex(cacheKey, 3600, JSON.stringify(user));
  
  res.json(user);
});

GraphQL: Normalized Caching

import { InMemoryCache, ApolloClient } from '@apollo/client';

const cache = new InMemoryCache({
  typePolicies: {
    User: {
      keyFields: ['id'],
      fields: {
        posts: {
          merge(existing, incoming) {
            return incoming;
          }
        }
      }
    },
    Post: {
      keyFields: ['id']
    }
  }
});

const client = new ApolloClient({
  cache,
  uri: 'http://localhost:4000/graphql'
});

// After query, data is cached and normalized
// Subsequent queries can use cached data
const { data } = await client.query({
  query: GET_USER,
  variables: { id: '123' },
  fetchPolicy: 'cache-first' // Use cache if available
});

When to Choose REST

Best Use Cases

// 1. Simple CRUD operations
// Resources are clearly defined and map well to URLs
app.get('/api/products');      // List products
app.get('/api/products/123'); // Get product
app.post('/api/products');     // Create product
app.put('/api/products/123');  // Update product
app.delete('/api/products/123');

// 2. Public APIs with rate limiting
// Standard HTTP caching, CDN-friendly
app.get('/api/public/leaderboard', (req, res) => {
  res.set('Cache-Control', 'public, max-age=60');
  res.json(getLeaderboard());
});

// 3. Microservices with clear boundaries
// Each service owns its resources
// /users-service/api/users
// /orders-service/api/orders
// /inventory-service/api/products

REST Advantages

Advantage Description
Simplicity Easy to understand, widely adopted
HTTP Caching Native support with ETag, Last-Modified
CDN Support Works seamlessly with content delivery networks
Stateless Each request contains all information
Standardized HTTP methods, status codes, headers
Tooling Extensive client libraries, API tools

REST Disadvantages

Disadvantage Description
Over-fetching Returns fixed data structure
Multiple Round-trips Deep nesting requires many requests
Versioning API versions in URL or headers
Documentation Endpoint documentation needs maintenance

When to Choose GraphQL

Best Use Cases

// 1. Mobile apps with limited bandwidth
// Request exactly what UI needs
const query = `
  query MobileProductList {
    products {
      id
      name
      price
      image
    }
  }
`;

// 2. Complex data relationships
// Nested queries without multiple requests
const query = `
  query ComplexDashboard {
    user {
      name
      posts {
        title
        comments {
          author { name }
          text
        }
      }
    }
  }
`;

// 3. Rapid frontend development
// Frontend can evolve independently
// Add new fields without backend changes

GraphQL Advantages

Advantage Description
Flexible Queries Client requests exactly needed data
Single Request Fetch multiple resources in one call
Strong Typing Schema defines types, self-documenting
Real-time Subscriptions for live updates
Schema Evolution Additive changes without versioning

GraphQL Disadvantages

Disadvantage Description
Complexity Steeper learning curve
Caching More complex than HTTP caching
File Upload Not natively supported
N+1 Problem Can cause excessive database queries
Rate Limiting Harder to implement per-field limits

Decision Framework

flowchart TD
    A[Start] --> B{Simple CRUD?}
    B -->|Yes| C[Use REST]
    B -->|No| D{Mobile/limited bandwidth?}
    D -->|Yes| E[Use GraphQL]
    D -->|No| F{Multiple clients?}
    F -->|Yes| G[GraphQL]
    F -->|No| H{Complex relationships?}
    H -->|Yes| I[GraphQL]
    H -->|No| J{Real-time needed?}
    J -->|Yes| K[GraphQL + Subscriptions]
    J -->|No| L{Rate limiting critical?}
    L -->|Yes| M[REST]
    L -->|No| N{Cache-heavy?}
    N -->|Yes| O[REST]
    N -->|No| P[Either works - consider team expertise]

Quick Decision Guide

Scenario Recommendation
Public API REST
Internal microservice REST or gRPC
Mobile app GraphQL
Complex dashboard GraphQL
Real-time updates GraphQL + Subscriptions
Simple CRUD REST
CDN caching REST
Team unfamiliar with GraphQL REST

Hybrid Approach

// Use REST for simple, cacheable resources
// Use GraphQL for complex, dynamic queries

const express = require('express');
const { ApolloServer } = require('apollo-server-express');
const app = express();

// REST endpoints for simple resources
app.get('/api/status', (req, res) => {
  res.json({ status: 'ok', timestamp: Date.now() });
});

app.get('/api/public/leaderboard', cache(60), (req, res) => {
  res.json(getLeaderboard());
});

// GraphQL for complex queries
const server = new ApolloServer({ typeDefs, resolvers });
await server.start();
server.applyMiddleware({ app, path: '/graphql' });

app.listen(3000);

Best Practices

REST Best Practices

// Use consistent naming
GET    /api/users          // Plural nouns
GET    /api/users/123      // Resource ID in URL
GET    /api/users/123/posts  // Nested resources

// Use query parameters for filtering, sorting, pagination
GET /api/users?role=admin&sort=name&limit=10&offset=20

// Use proper status codes
200 - OK
201 - Created
204 - No Content (successful delete)
400 - Bad Request
401 - Unauthorized
403 - Forbidden
404 - Not Found
500 - Internal Server Error

// Version your API
GET /api/v1/users
GET /api/v2/users

GraphQL Best Practices

// Schema-first design
const typeDefs = gql`
  type Query {
    # Clear, descriptive names
    activeUsers: [User!]!
  }
  
  type User {
    # Always include ID
    id: ID!
    
    # Descriptive field names
    createdAt: DateTime!
    postCount: Int!
  }
`;

// Use fragments to share fields
const UserFields = `
  fragment UserFields on User {
    id
    name
    email
  }
`;

// Connection pattern for pagination
const typeDefs = gql`
  type UserConnection {
    edges: [UserEdge!]!
    pageInfo: PageInfo!
    totalCount: Int!
  }
  
  type UserEdge {
    node: User!
    cursor: String!
  }
  
  type PageInfo {
    hasNextPage: Boolean!
    hasPreviousPage: Boolean!
    startCursor: String
    endCursor: String
  }
`;

// Handle N+1 with DataLoader
const DataLoader = require('dataloader');

const userLoader = new DataLoader(async (ids) => {
  const users = await db.users.findByIds(ids);
  return ids.map(id => users.find(u => u.id === id));
});

// In resolver
User: {
  async posts(user) {
    return postLoader.load(user.id);
  }
}

External Resources

Comments