GraphQL vs REST: Choosing the Right API Paradigm

Compare GraphQL and REST APIs, understand when to use each approach, schema design, queries, mutations, and trade-offs between the two paradigms.

published: reading time: 10 min read

GraphQL vs REST: Choosing the Right API Paradigm

GraphQL and REST are both approaches for building APIs, but they differ fundamentally in how clients interact with data. Understanding the trade-offs helps you choose the right approach for your project.

The RESTful API Design post covers REST principles. This post compares both approaches directly.


What is GraphQL?

GraphQL is a query language for APIs, developed by Facebook in 2012 and open-sourced in 2015. Instead of having multiple endpoints for different resources, you have a single endpoint that accepts queries.

graph LR
    A[Client] -->|"POST /graphql"| B[GraphQL Server]
    B --> C[Schema]
    C --> D[Resolvers]
    D --> E[Data Sources]

The client specifies exactly what data it needs in the query. The server returns exactly that data, nothing more.


Core Differences

REST: Multiple Endpoints

REST uses different endpoints for different resources:

# REST: Multiple endpoints
GET /users/123
GET /users/123/posts
GET /users/123/followers

Each endpoint returns a fixed data structure. If you need a user’s posts with follower counts, you might need multiple requests or get more data than necessary.

GraphQL: Single Endpoint

GraphQL uses a single endpoint with flexible queries:

# GraphQL: Single endpoint, flexible query
POST /graphql

query {
  user(id: 123) {
    name
    posts {
      title
    }
    followersCount
  }
}

One request gets exactly the data you need.


Schema and Types

GraphQL uses a strongly-typed schema to define available data and operations:

type User {
  id: ID!
  name: String!
  email: String!
  posts: [Post!]!
  followersCount: Int!
}

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

type Query {
  user(id: ID!): User
  posts(limit: Int): [Post!]!
}

type Mutation {
  createPost(title: String!, content: String!): Post!
}

The schema serves as a contract between client and server. Clients know exactly what data is available.

REST and Schemas

REST does not enforce a schema. You document your API and clients hope for consistency. OpenAPI specification helps, but it is not enforced at runtime.


Queries

REST Queries

# REST: Get user and their posts
GET /users/123
GET /users/123/posts

GraphQL Queries

# GraphQL: One request, precise data
query GetUserWithPosts($userId: ID!) {
  user(id: $userId) {
    name
    email
    posts {
      title
      createdAt
    }
  }
}

GraphQL queries run in parallel automatically. If you request multiple fields, GraphQL fetches them concurrently.


Mutations

REST Mutations

# REST: Create, update, delete with different HTTP methods
POST /users
PUT /users/123
DELETE /users/123

GraphQL Mutations

# GraphQL: Mutations are explicit
mutation CreateUser($input: CreateUserInput!) {
  createUser(input: $input) {
    id
    name
    email
  }
}

mutation UpdateUser($id: ID!, $input: UpdateUserInput!) {
  updateUser(id: $id, input: $input) {
    id
    name
    email
  }
}

mutation DeleteUser($id: ID!) {
  deleteUser(id: $id)
}

Data Fetching

Overfetching and Underfetching

REST often leads to overfetching (getting more data than needed) or underfetching (needing multiple requests):

# REST: Gets more data than needed
GET /users/123
# Returns: { id, name, email, created_at, updated_at, profile_url, bio, ... }

# REST: Multiple requests for related data
GET /users/123          # User info
GET /users/123/posts    # User's posts
GET /posts/456/comments # Comments for a specific post

GraphQL solves both problems:

# GraphQL: Exact data
query {
  user(id: 123) {
    name # Only what you need
    posts {
      title # Only what you need
    }
  }
}

N+1 Problem

GraphQL can suffer from the N+1 problem: fetching a list of users, then making a separate request for each user’s posts:

# This could trigger many database queries
query {
  users {
    name
    posts {
      title # Triggers query for each user's posts
    }
  }
}

DataLoader solves this by batching requests.


Error Handling

REST Errors

REST uses HTTP status codes:

HTTP/1.1 404 Not Found
Content-Type: application/json

{"error": "User not found"}

GraphQL Errors

GraphQL returns 200 OK even for errors. Errors are in the response body:

{
  "data": null,
  "errors": [
    {
      "message": "User not found",
      "locations": [{ "line": 3, "column": 5 }],
      "path": ["user"]
    }
  ]
}

This is controversial. Some prefer HTTP status codes for errors.


Caching

REST Caching

REST works well with HTTP caching:

GET /users/123
Cache-Control: max-age=3600
ETag: "v1"

CDNs, browser caches, and libraries like React Query handle REST caching well.

GraphQL Caching

GraphQL POST requests are harder to cache by default. Solutions:

  • Normalized caching with Apollo Client or Relay
  • Persisted queries that become GET requests
  • Response caching at the CDN level

When to Choose REST

REST works well when:

  • Your API is simple with predictable data requirements
  • You need HTTP caching
  • You are building public APIs consumed by many clients
  • Your team is familiar with REST
  • You need simple documentation (just list endpoints)

Examples: CRUD applications, simple CRUD APIs, public APIs for third-party developers

When to Choose GraphQL

GraphQL works well when:

  • Clients need different data shapes
  • Mobile apps needing minimal data transfer
  • Complex domains with many related entities
  • Rapid iteration with frontend teams
  • You want strong typing and schema validation

Examples: Mobile apps, complex dashboards, microservices with varying client needs


Comparison Table

AspectRESTGraphQL
Data fetchingMultiple requestsSingle request
Data shapeFixed per endpointClient specifies
TypingDocumentationSchema enforced
CachingHTTP cachingCustom caching
Learning curveLowerHigher
ToolingMatureEvolving
Error handlingHTTP status codes200 + error body
OverfetchingCommonAvoided

Combining Both

You do not have to choose one exclusively. Some teams use REST for simple operations and GraphQL for complex data requirements.

# REST for simple operations
GET /health
GET /config

# GraphQL for complex data
POST /graphql

Production Failure Scenarios

FailureImpactMitigation
Query complexity explosionServer overwhelmed; possible DoSImplement query depth limiting; set complexity budgets
N+1 query problemDatabase flooded with queries; slow responsesUse DataLoader for batching; optimize resolvers
Schema introspection abuseInformation leakage; enumeration attacksDisable introspection in production; restrict access
Subscription memory leaksServer memory grows; eventual crashSet subscription limits; implement timeouts
Mutation race conditionsData inconsistency between related operationsImplement optimistic locking; use transactions
Persisted query abuseAttackers pre-store malicious queriesValidate persisted query hashes; rate limit
Error masking with 200 OKErrors hidden; debugging difficultReturn proper error status codes; use extensions

Observability Checklist

Metrics

  • Query rate by operation type (query/mutation/subscription)
  • Query complexity distribution
  • Request duration by operation and field
  • Error rate by error type
  • DataLoader batch efficiency (cache hit ratio)
  • Schema introspection requests
  • Subscription active count
  • Query depth and breadth distribution

Logs

  • Query requests with variables and complexity
  • Mutation requests with authorization context
  • DataLoader batch operations and cache misses
  • Error responses with path and locations
  • Schema change events
  • Subscription lifecycle events
  • Security events (introspection attempts, rate limit hits)

Alerts

  • Query complexity exceeds threshold
  • Error rate exceeds normal baseline
  • DataLoader cache hit ratio drops below 80%
  • Subscription count exceeds limits
  • Unusual introspection activity
  • Query depth spikes indicate potential attack

Security Checklist

  • Disable schema introspection in production
  • Implement query complexity limits and depth limits
  • Use persisted queries to prevent abuse
  • Validate and sanitize all variable inputs
  • Implement proper authorization at resolver level
  • Log and monitor unusual query patterns
  • Rate limit queries per client
  • Protect against query batching attacks (array literals)
  • Use query whitelisting for sensitive operations
  • Validate that mutations affect only intended fields
  • Implement request timeout at GraphQL layer
  • Do not expose internal error details in responses

Common Pitfalls / Anti-Patterns

Overusing GraphQL for Simple Cases

GraphQL adds complexity. Simple REST endpoints may be better.

# Overkill: GraphQL for simple, predictable data
query {
  healthCheck {
    status
  }
}

# Better: Simple REST endpoint
GET /health

Ignoring N+1 Queries

GraphQL makes N+1 problems easy to create.

# Problem: Fetches posts for each user separately
query {
  users {
    name
    posts {
      title
    } # N queries for N users
  }
}

# Better: Use DataLoader to batch
query {
  users {
    name
    posts {
      title
    } # Single batched query
  }
}

Not Implementing Proper Error Handling

GraphQL returns 200 OK even for errors.

// Problem: Error masked as success
{
  "data": { "user": null },
  "errors": [{ "message": "Not authorized" }]
}

// Better: Use proper HTTP status codes
{
  "errors": [{
    "extensions": { "code": "UNAUTHORIZED" }
  }]
}

Exposing Schema Internals

Introspection can reveal your entire schema.

// Disable introspection in production
const server = new ApolloServer({
  schema,
  introspection: false, // Production
  playground: false, // Production
});

Quick Recap

Key Bullets

  • REST uses fixed endpoints returning predictable data; GraphQL uses single endpoint with flexible queries
  • GraphQL eliminates overfetching and underfetching but adds complexity
  • GraphQL schemas provide type safety and self-documenting APIs
  • REST works well with HTTP caching; GraphQL requires custom caching strategies
  • GraphQL subscriptions enable real-time updates
  • N+1 queries are a common GraphQL performance issue; DataLoader solves this
  • Both can coexist; use REST for simple cases, GraphQL for complex data requirements
  • Error handling differs: REST uses HTTP status codes, GraphQL uses 200 with error bodies

Copy/Paste Checklist

# Test GraphQL query with curl
curl -X POST https://api.example.com/graphql \
  -H "Content-Type: application/json" \
  -d '{"query":"{ user(id: 1) { name email } }"}'

# Test GraphQL mutation
curl -X POST https://api.example.com/graphql \
  -H "Content-Type: application/json" \
  -d '{"query":"mutation { createUser(input: {name: \"Alice\"}) { id } }"}'

# Introspection query (disable in production!)
curl -X POST https://api.example.com/graphql \
  -H "Content-Type: application/json" \
  -d '{"query":"{ __schema { types { name } } }"}'

# Check query complexity
# Use Apollo Studio or GraphQL Playground analysis tools

Conclusion

REST and GraphQL represent different philosophies. REST is resource-oriented with fixed endpoints. GraphQL is query-oriented with flexible data fetching.

REST shines for simple, predictable APIs where caching matters. GraphQL shines for complex, client-driven data requirements where you want to avoid overfetching. Neither is universally better. Choose based on your specific needs.

For REST API design, see the RESTful API Design post. For versioning both types of APIs, see the API Versioning Strategies post.

Category

Related Posts

RESTful API Design: Best Practices for Building Web APIs

Learn REST principles, resource naming, HTTP methods, status codes, and best practices. Design clean, maintainable, and scalable RESTful APIs.

#api #rest #architecture

API Versioning Strategies: Managing API Evolution Without Breaking Clients

Learn different API versioning strategies including URL versioning, header versioning, and query parameters. Understand backward compatibility and deprecation practices.

#api #versioning #rest

Rate Limiting: Token Bucket, Sliding Window, and Distributed

Rate limiting protects APIs from abuse. Learn token bucket, sliding window, fixed window algorithms and distributed rate limiting at scale.

#api #rate-limiting #architecture