Back to Blog
GraphQL vs REST API: Complete Comparison Guide for 2024

GraphQL vs REST API: Complete Comparison Guide for 2024

December 11, 2024
9 min read
Tushar Agrawal

Understand when to use GraphQL vs REST API. Compare performance, flexibility, caching, real-world use cases, and learn practical implementation with code examples in Python and Node.js.

Introduction

The GraphQL vs REST debate continues to be one of the most discussed topics in API design. Having implemented both at scale—REST APIs handling millions of requests and GraphQL for complex data requirements—I'll share practical insights to help you make the right choice.

Quick Comparison

| Aspect | REST | GraphQL | |--------|------|---------| | Data Fetching | Fixed endpoints | Flexible queries | | Over-fetching | Common problem | Solved | | Under-fetching | Multiple requests | Single request | | Caching | HTTP caching | Complex, custom | | Learning Curve | Lower | Higher | | Tooling | Mature ecosystem | Growing rapidly | | File Upload | Native support | Needs workarounds | | Real-time | Requires WebSocket | Subscriptions built-in |

REST API Overview

Traditional REST Approach

REST endpoints for a blog

GET /api/posts # List all posts GET /api/posts/123 # Get single post POST /api/posts # Create post PUT /api/posts/123 # Update post DELETE /api/posts/123 # Delete post

Nested resources

GET /api/posts/123/comments # Get post comments GET /api/users/456/posts # Get user's posts

REST Implementation (Python/FastAPI)

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List, Optional

app = FastAPI()

class Post(BaseModel): id: int title: str content: str author_id: int tags: List[str] created_at: str

class PostCreate(BaseModel): title: str content: str tags: List[str] = []

@app.get("/api/posts", response_model=List[Post]) async def list_posts( page: int = 1, limit: int = 10, author_id: Optional[int] = None ): # Returns ALL fields even if client only needs title posts = await db.get_posts(page, limit, author_id) return posts

@app.get("/api/posts/{post_id}", response_model=Post) async def get_post(post_id: int): post = await db.get_post(post_id) if not post: raise HTTPException(status_code=404, detail="Post not found") return post

@app.get("/api/posts/{post_id}/comments") async def get_post_comments(post_id: int): # Separate request needed for comments return await db.get_comments(post_id)

REST Problems: Over-fetching and Under-fetching

// Client needs: post title + author name + comment count

// Problem 1: Over-fetching // GET /api/posts/123 returns everything: { "id": 123, "title": "My Post", // ✓ needed "content": "Very long...", // ✗ not needed (wasted bandwidth) "author_id": 456, // need author NAME, not just ID "tags": ["tech", "api"], // ✗ not needed "created_at": "2024-01-01", // ✗ not needed "updated_at": "2024-01-02", // ✗ not needed "view_count": 1000, // ✗ not needed "likes": 50 // ✗ not needed }

// Problem 2: Under-fetching (need multiple requests) // Request 1: GET /api/posts/123 // Request 2: GET /api/users/456 (to get author name) // Request 3: GET /api/posts/123/comments (to count)

// 3 HTTP round-trips for one screen!

GraphQL Overview

GraphQL Query Language

Define schema

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

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

type Comment { id: ID! content: String! author: User! }

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

type Mutation { createPost(input: CreatePostInput!): Post! updatePost(id: ID!, input: UpdatePostInput!): Post! deletePost(id: ID!): Boolean! }

GraphQL Query - Get Exactly What You Need

Client queries only what's needed

query GetPostDetails($postId: ID!) { post(id: $postId) { title # ✓ just what we need author { name # ✓ author name directly } comments { id # ✓ can count these } } }

Response - no over-fetching!

{ "data": { "post": { "title": "My Post", "author": { "name": "John Doe" }, "comments": [ { "id": "1" }, { "id": "2" } ] } } }

ONE request instead of three!

GraphQL Implementation (Python/Strawberry)

import strawberry
from strawberry.fastapi import GraphQLRouter
from typing import List, Optional

@strawberry.type class User: id: strawberry.ID name: str email: str

@strawberry.field async def posts(self, info) -> List["Post"]: # Lazy loading - only fetched if requested return await info.context.loaders.user_posts.load(self.id)

@strawberry.type class Comment: id: strawberry.ID content: str author: User

@strawberry.type class Post: id: strawberry.ID title: str content: str

@strawberry.field async def author(self, info) -> User: # DataLoader prevents N+1 queries return await info.context.loaders.users.load(self.author_id)

@strawberry.field async def comments(self, info) -> List[Comment]: return await info.context.loaders.post_comments.load(self.id)

@strawberry.type class Query: @strawberry.field async def post(self, id: strawberry.ID, info) -> Optional[Post]: return await info.context.db.get_post(id)

@strawberry.field async def posts( self, info, page: int = 1, limit: int = 10 ) -> List[Post]: return await info.context.db.get_posts(page, limit)

@strawberry.type class Mutation: @strawberry.mutation async def create_post( self, title: str, content: str, info ) -> Post: user = info.context.user return await info.context.db.create_post(user.id, title, content)

schema = strawberry.Schema(query=Query, mutation=Mutation) graphql_app = GraphQLRouter(schema)

Performance Comparison

Network Efficiency

REST (3 requests for post details):
┌─────────┐     ┌─────────┐
│ Client  │────►│ Server  │  GET /posts/123        ~50ms
│         │◄────│         │
│         │────►│         │  GET /users/456        ~50ms
│         │◄────│         │
│         │────►│         │  GET /posts/123/comments ~50ms
│         │◄────│         │
└─────────┘     └─────────┘
Total: ~150ms + overhead

GraphQL (1 request): ┌─────────┐ ┌─────────┐ │ Client │────►│ Server │ POST /graphql ~80ms │ │◄────│ │ (all data in one trip) └─────────┘ └─────────┘ Total: ~80ms

Caching Differences

REST - Easy HTTP Caching

GET /api/posts/123

Response headers:

Cache-Control: max-age=3600

ETag: "abc123"

Browser/CDN automatically caches based on URL

GraphQL - Complex Caching

All requests go to POST /graphql

Same URL, different queries - can't use HTTP caching directly

Solutions for GraphQL caching:

1. Persisted queries (query hash as ID)

2. Client-side normalized caching (Apollo, Relay)

3. CDN with query parsing (like Apollo Router)

Apollo Client example

const client = new ApolloClient({ cache: new InMemoryCache({ typePolicies: { Post: { keyFields: ["id"], # Normalize by ID }, }, }), });

When to Use REST

1. Simple CRUD Operations

REST excels at straightforward CRUD

Clean, predictable, easy to understand

@app.post("/api/users") async def create_user(user: UserCreate): return await db.create_user(user)

@app.get("/api/users/{id}") async def get_user(id: int): return await db.get_user(id)

@app.put("/api/users/{id}") async def update_user(id: int, user: UserUpdate): return await db.update_user(id, user)

@app.delete("/api/users/{id}") async def delete_user(id: int): return await db.delete_user(id)

2. Public APIs

REST advantages for public APIs:
✓ Universally understood
✓ Easy documentation (OpenAPI/Swagger)
✓ Works with any HTTP client
✓ Simple rate limiting per endpoint
✓ Cacheable by CDNs

3. File Uploads

REST - Native multipart support

@app.post("/api/upload") async def upload_file(file: UploadFile): content = await file.read() url = await storage.save(file.filename, content) return {"url": url}

GraphQL requires workarounds

- Base64 encoding (inefficient)

- Separate REST endpoint

- graphql-upload spec (adds complexity)

4. Microservices Communication

REST for internal service-to-service

Simple, fast, well-understood

Service A ──HTTP/REST──► Service B ──HTTP/REST──► Service C ──HTTP/REST──► Service D

Easy to implement, debug, and monitor

When to Use GraphQL

1. Complex, Nested Data Requirements

One query to get complex nested data

query DashboardData { currentUser { name notifications(unread: true) { message createdAt } recentOrders(limit: 5) { id status items { product { name image } quantity } } } featuredProducts(limit: 10) { name price reviews { rating } } }

REST would need 4-5 separate requests

2. Mobile Applications

GraphQL advantages for mobile:
✓ Minimize bandwidth usage
✓ Reduce battery drain (fewer requests)
✓ Adapt to different screen sizes
✓ Offline-first with normalized cache

Phone gets minimal data

query MobilePostList { posts { id, title, thumbnail } }

Tablet gets more

query TabletPostList { posts { id, title, excerpt, thumbnail, author { name } } }

3. Rapidly Evolving Frontend

Frontend can request new fields without backend changes

(as long as field exists in schema)

Version 1

query { user { name, email } }

Version 2 - Added avatar, no backend change needed

query { user { name, email, avatar } }

Version 3 - Added social links

query { user { name, email, avatar, socialLinks { twitter, linkedin } } }

4. Real-time Features

GraphQL Subscriptions for real-time

subscription OnNewMessage($chatId: ID!) { messageAdded(chatId: $chatId) { id content sender { name avatar } createdAt } }

Client receives updates automatically

Built into GraphQL spec

Hybrid Approach

Best of Both Worlds:
┌─────────────────────────────────────────────┐
│                  API Gateway                 │
└──────────────────┬──────────────────────────┘
                   │
     ┌─────────────┼─────────────┐
     │             │             │
     ▼             ▼             ▼
┌─────────┐  ┌──────────┐  ┌─────────┐
│  REST   │  │ GraphQL  │  │  REST   │
│ /upload │  │ /graphql │  │ /health │
│ /webhook│  │          │  │ /metrics│
└─────────┘  └──────────┘  └─────────┘

Use REST for:

  • File uploads
  • Webhooks
  • Health checks
  • Simple public APIs

Use GraphQL for:

  • Complex data fetching
  • Frontend-driven queries
  • Real-time features

N+1 Problem and Solutions

The N+1 Problem

Query: Get 10 posts with their authors

Without DataLoader:

posts = db.get_posts(limit=10) # 1 query for post in posts: post.author = db.get_user(post.author_id) # 10 queries!

Total: 11 queries (1 + N)

With DataLoader:

from strawberry.dataloader import DataLoader

async def load_users(user_ids: List[int]) -> List[User]: # Batch all user IDs into ONE query users = await db.get_users_by_ids(user_ids) # Return in same order as input user_map = {u.id: u for u in users} return [user_map.get(id) for id in user_ids]

user_loader = DataLoader(load_fn=load_users)

Now:

posts = db.get_posts(limit=10) # 1 query authors = await user_loader.load_many([p.author_id for p in posts]) # 1 query

Total: 2 queries!

Security Considerations

GraphQL-Specific Security

1. Query Depth Limiting

from graphql import GraphQLError

def depth_limit_validator(max_depth: int): def validate(query_ast): depth = calculate_depth(query_ast) if depth > max_depth: raise GraphQLError(f"Query depth {depth} exceeds max {max_depth}") return validate

2. Query Complexity Analysis

@strawberry.type class Query: @strawberry.field(extensions=[ComplexityExtension(cost=10)]) async def expensive_query(self) -> List[Post]: pass

3. Rate Limiting by Complexity

Instead of requests/minute, limit by total complexity points

4. Disable Introspection in Production

schema = strawberry.Schema( query=Query, mutation=Mutation, extensions=[ DisableIntrospection() if not DEBUG else None ] )

Key Takeaways

| Choose REST When | Choose GraphQL When | |-----------------|---------------------| | Simple CRUD apps | Complex data needs | | Public APIs | Mobile-first apps | | File handling | Rapid iteration | | Microservices | Real-time features | | HTTP caching critical | Bandwidth critical | | Team new to APIs | Team experienced |

Conclusion

There's no universal winner. REST remains excellent for simple, public, and cache-heavy APIs. GraphQL shines with complex data requirements and mobile applications. Many successful companies use both—REST for simplicity where it fits, GraphQL for complex frontend needs. Choose based on your specific requirements, team expertise, and use case.

---

Building APIs? Connect on LinkedIn to discuss architecture decisions.

Share this article

Related Articles