Choosing the right API paradigm is one of the most important architectural decisions you’ll make. REST has been the standard for years, but GraphQL has emerged as a powerful alternative that addresses many of REST’s limitations.
In this guide, we’ll compare GraphQL and REST, explore their strengths and weaknesses, and help you decide which approach fits your use case.
Understanding REST APIs
REST (Representational State Transfer) is an architectural style that uses HTTP methods to perform CRUD operations on resources.
REST Fundamentals
# REST API Endpoints
# GET - Retrieve resources
GET /api/users # List all users
GET /api/users/123 # Get specific user
GET /api/users/123/orders # Get user's orders
# POST - Create resources
POST /api/users # Create new user
# PUT - Update resources (full update)
PUT /api/users/123 # Update entire user
# PATCH - Partial update
PATCH /api/users/123 # Update specific fields
# DELETE - Remove resources
DELETE /api/users/123 # Delete user
REST Response Format
// REST JSON Response
{
"user": {
"id": "123",
"name": "John Doe",
"email": "[email protected]",
"created_at": "2025-01-15T10:30:00Z"
}
}
// REST List Response with Pagination
{
"data": [
{"id": "1", "name": "User 1"},
{"id": "2", "name": "User 2"}
],
"pagination": {
"page": 1,
"per_page": 20,
"total": 100,
"pages": 5
}
}
REST Best Practices
# REST API Design Principles
naming_conventions:
- Use nouns for resources: /users, /orders, /products
- Use plural forms: /users not /user
- Use kebab-case: /user-profiles not /userProfiles
- Nested resources for relationships: /users/123/orders
http_status_codes:
"200": "OK - Success"
"201": "Created - Resource created"
"204": "No Content - Successful deletion"
"400": "Bad Request - Invalid input"
"401": "Unauthorized - Authentication required"
"403": "Forbidden - No permission"
"404": "Not Found - Resource doesn't exist"
"500": "Internal Server Error"
versioning:
- URL versioning: /api/v1/users
- Header versioning: Accept: application/vnd.api.v1+json
- Query versioning: /api/users?version=1
Understanding GraphQL
GraphQL is a query language for APIs that allows clients to request exactly the data they need.
GraphQL Query Structure
# Simple GraphQL Query
query {
user(id: "123") {
name
email
}
}
# Response
{
"data": {
"user": {
"name": "John Doe",
"email": "[email protected]"
}
}
}
GraphQL Schema Definition
# Define Types
type User {
id: ID!
name: String!
email: String!
age: Int
createdAt: DateTime!
orders: [Order!]!
}
type Order {
id: ID!
userId: ID!
total: Float!
items: [OrderItem!]!
status: OrderStatus!
createdAt: DateTime!
}
enum OrderStatus {
PENDING
PAID
SHIPPED
DELIVERED
CANCELLED
}
type OrderItem {
productId: ID!
product: Product!
quantity: Int!
price: Float!
}
type Product {
id: ID!
name: String!
price: Float!
}
# Define Queries (Read)
type Query {
user(id: ID!): User
users(limit: Int, offset: Int): [User!]!
order(id: ID!): Order
orders(userId: ID): [Order!]!
}
# Define Mutations (Write)
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
deleteUser(id: ID!): Boolean!
createOrder(input: CreateOrderInput!): Order!
}
input CreateUserInput {
name: String!
email: String!
age: Int
}
input UpdateUserInput {
name: String
email: String
age: Int
}
# Define Subscriptions (Real-time)
type Subscription {
orderUpdated(userId: ID!): Order!
}
GraphQL with Python (Strawberry)
import strawberry
from typing import List, Optional
from datetime import datetime
@strawberry.type
class User:
id: strawberry.ID
name: str
email: str
age: Optional[int] = None
created_at: datetime
@strawberry.field
def orders(self) -> List["Order"]:
return get_orders_for_user(self.id)
@strawberry.type
class Query:
@strawberry.field
def user(self, id: strawberry.ID) -> Optional[User]:
return db.get_user(id)
@strawberry.field
def users(self, limit: int = 20, offset: int = 0) -> List[User]:
return db.get_users(limit=limit, offset=offset)
@strawberry.type
class Mutation:
@strawberry.mutation
def create_user(self, name: str, email: str) -> User:
user = User(name=name, email=email)
return db.save_user(user)
schema = strawberry.Schema(query=Query, mutation=Mutation)
REST vs GraphQL: Key Differences
Data Fetching Comparison
# REST: Multiple endpoints, over-fetching
# Request 1: Get user info
GET /api/users/123
# Returns: {id, name, email, age, created_at, ...}
# Request 2: Get user's orders (with items)
GET /api/users/123/orders
# Returns: [{id, total, items: [...], ...}]
# Request 3: Get product details for each item
GET /api/products/1
GET /api/products/2
# Total: 4 HTTP requests, lots of unused data
# GraphQL: Single request, exact data
query {
user(id: "123") {
name
orders {
total
items {
product {
name
price
}
quantity
}
}
}
}
# Returns exactly what was requested
# Single HTTP request
Comparison Table
| Aspect | REST | GraphQL |
|---|---|---|
| Data Fetching | Multiple endpoints | Single request |
| Over-fetching | Common | Eliminated |
| Under-fetching | Common | Eliminated |
| Caching | HTTP caching | Custom caching |
| Learning Curve | Lower | Higher |
| Type Safety | Optional | Built-in |
| Real-time | WebSockets separately | Subscriptions |
| File Upload | Native | Complex |
| Caching | HTTP cache | Normalized cache |
When to Use REST
# REST is better when:
use_cases:
- Simple CRUD operations
- Public APIs with documentation
- Team unfamiliar with GraphQL
- HTTP caching is important
- Microservices with clear boundaries
- Bandwidth-constrained environments
- Already existing REST infrastructure
examples:
- Banking APIs (strict standards)
- Payment gateways
- Mobile apps with limited data needs
- Simple content APIs
REST Example: E-commerce API
# RESTful E-commerce API
# Products
GET /api/products
GET /api/products/{id}
POST /api/products
PUT /api/products/{id}
DELETE /api/products/{id}
# Orders
GET /api/orders
GET /api/orders/{id}
POST /api/orders
PATCH /api/orders/{id}/cancel
# Cart
GET /api/cart
POST /api/cart/items
PATCH /api/cart/items/{product_id}
DELETE /api/cart/items/{product_id}
DELETE /api/cart
When to Use GraphQL
# GraphQL is better when:
use_cases:
- Complex data relationships
- Mobile apps (bandwidth saving)
- Rapid frontend development
- Aggregating multiple services
- Highly interactive UIs
- Team owns full stack
- Schema evolution is important
examples:
- Social media apps
- Dashboard applications
- BFF (Backend for Frontend) patterns
- Data aggregation layers
GraphQL Example: Social Media API
# GraphQL for Social Media
# Single query can fetch:
# - User profile
# - Their posts
# - Comments on posts
# - Likes count
# - Follower count
# - Recent activity
query GetFeed($userId: ID!) {
user(id: $userId) {
name
avatar
posts(limit: 10) {
content
images
likes {
count
recentUsers {
name
avatar
}
}
comments {
text
author {
name
}
}
}
stats {
followers
following
postsCount
}
}
}
Hybrid Approach: Using Both
Many successful APIs use both REST and GraphQL:
# Hybrid API Architecture
class HybridAPI:
"""
Use REST for:
- Simple CRUD operations
- File uploads/downloads
- Webhooks
- Authentication endpoints
Use GraphQL for:
- Complex queries
- Real-time updates
- Dashboard data
- Mobile app data
"""
# REST for Auth
REST_ENDPOINTS = {
"POST /auth/login": "User login",
"POST /auth/logout": "User logout",
"POST /auth/refresh": "Refresh token",
}
# REST for Files
REST_ENDPOINTS = {
"POST /upload": "Upload file",
"GET /download/{id}": "Download file",
}
# GraphQL for Data
GRAPHQL_SCHEMA = """
type Query {
# Complex queries here
}
type Mutation {
# Complex mutations here
}
"""
API Design Patterns
Pagination
# REST Cursor-based Pagination
class CursorPagination:
"""
Use cursor for large datasets
"""
def get_items(self, cursor=None, limit=20):
return db.query("""
SELECT * FROM users
WHERE cursor > ?
ORDER BY cursor
LIMIT ?
""", [cursor, limit])
# REST Offset-based Pagination
class OffsetPagination:
def get_items(self, page=1, per_page=20):
offset = (page - 1) * per_page
return db.query("""
SELECT * FROM users
ORDER BY id
LIMIT ? OFFSET ?
""", [per_page, offset])
# GraphQL Connection Specification
pagination_schema = """
type UserConnection {
edges: [UserEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type UserEdge {
node: User!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
"""
Error Handling
# REST Error Response
class RESTError:
@app.errorhandler(ValidationError)
def handle_validation(error):
return {
"error": {
"code": "VALIDATION_ERROR",
"message": str(error),
"details": error.details
}
}, 400
# GraphQL Error Response
error_schema = """
type Mutation {
createUser(input: UserInput!): CreateUserPayload
}
union CreateUserPayload = User | ValidationError
type ValidationError {
field: String!
message: String!
}
"""
API Versioning
# REST Versioning Strategies
# 1. URL Versioning (most common)
@app.route('/api/v1/users')
@app.route('/api/v2/users')
def get_users():
version = request.version
...
# 2. Header Versioning
@app.route('/api/users')
def get_users():
version = request.headers.get('API-Version', 'v1')
...
# GraphQL Evolution (Preferred over Versioning)
evolution_schema = """
type User {
id: ID!
name: String!
email: String! @deprecated(reason: "Use contactEmail instead")
contactEmail: String
}
"""
Rate Limiting
# REST Rate Limiting (using headers)
@app.route('/api/users')
@ratelimit(limit=100, per=60)
def get_users():
return {"users": [...]}
# Add rate limit headers
def rate_limiter(limit, per):
def decorator(f):
@wraps(f)
def wrapped(*args, **kwargs):
# Check limit
remaining, reset = check_rate_limit()
resp = make_response(f(*args, **kwargs))
resp.headers['X-RateLimit-Limit'] = str(limit)
resp.headers['X-RateLimit-Remaining'] = str(remaining)
resp.headers['X-RateLimit-Reset'] = str(reset)
return resp
return wrapped
return decorator
# GraphQL Rate Limiting
rate_limit_schema = """
directive @rateLimit(
limit: Int,
window: String
) on FIELD_DEFINITION
type Query {
users: [User!]! @rateLimit(limit: 100, window: "1m")
}
"""
Performance Optimization
REST Caching
# HTTP Caching with ETag
@app.route('/api/users/<id>')
def get_user(id):
user = db.get_user(id)
etag = md5(str(user.updated_at))
if request.if_none_match == etag:
return '', 304
response = jsonify(user)
response.headers['ETag'] = etag
response.headers['Cache-Control'] = 'max-age=3600'
return response
# Last-Modified Caching
@app.route('/api/users')
def get_users():
users = db.get_users()
last_modified = max(u.updated_at for u in users)
if request.if_modified_since:
if last_modified <= request.if_modified_since:
return '', 304
response = jsonify(users)
response.headers['Last-Modified'] = last_modified
return response
GraphQL DataLoader
# Solve N+1 problem with DataLoader
from dataloader import DataLoader
class UserLoader(DataLoader):
def batch_load_fn(self, user_ids):
users = db.get_users(user_ids)
# Return list in same order as user_ids
return [users.get(uid) for uid in user_ids]
class OrderLoader(DataLoader):
def batch_load_fn(self, user_ids):
orders = db.get_orders_for_users(user_ids)
# Group orders by user_id
orders_by_user = {}
for order in orders:
orders_by_user.setdefault(order.user_id, []).append(order)
return [orders_by_user.get(uid, []) for uid in user_ids]
# Use in resolvers
@strawberry.field
def user(self, info: Info) -> User:
loader = info.context['user_loader']
return loader.load(self.user_id)
Conclusion
Both REST and GraphQL have their place in modern API development:
- REST remains excellent for simple CRUD operations, public APIs, and scenarios where HTTP caching is crucial
- GraphQL excels in complex data scenarios, mobile apps, and when frontend teams need flexibility
The best choice depends on your specific requirements, team expertise, and existing infrastructure. Many successful applications use both approaches strategically.
Comments