#Tech#Web Development#Programming#API

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

FeatureGraphQLREST
EndpointsSingleMultiple
Data FetchingClient specifiesServer specifies
Over-fetchingNoCommon
Under-fetchingNoCommon
VersioningNo needRequired
Real-timeNative subscriptionsWebSockets/SSE
CachingComplexBuilt-in HTTP
Error HandlingPartial errorsHTTP status codes
Learning CurveSteeperShallower
ToolingStrongMature
CommunityGrowingLarge

When to Use Each

Use GraphQL When:

  1. Complex Data Requirements

    • Multiple related resources needed
    • Varying data requirements per screen
    • Nested data relationships
  2. Mobile Applications

    • Limited bandwidth
    • Need precise data control
    • Offline-first architecture
  3. Real-Time Features

    • Live updates
    • Subscriptions
    • Collaborative apps
  4. Rapid Development

    • Frequent schema changes
    • Evolving requirements
    • Multiple client teams

Use REST When:

  1. Simple CRUD Operations

    • Straightforward resource management
    • Standard create, read, update, delete
    • Predictable data access patterns
  2. Public APIs

    • Third-party integrations
    • Need simple access
    • Caching critical
  3. Resource-Based Applications

    • Clear resource boundaries
    • Simple relationships
    • Stateless operations
  4. 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

  1. GraphQL for flexibility, efficiency, real-time
  2. REST for simplicity, caching, public APIs
  3. Hybrid approaches can leverage both
  4. Consider data requirements, client types, team expertise
  5. Test thoroughly before committing
  6. Plan migration strategy if changing approach
  7. Monitor performance and iterate

Next Steps

  1. Analyze your API requirements
  2. Prototype both approaches
  3. Evaluate with your team
  4. Choose based on use case
  5. Plan implementation roadmap
  6. Test thoroughly
  7. 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.