Skip to main content
โšก Calmops

GraphQL vs REST: API Design Patterns and Best Practices

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