Your API is your product's interface to the world. REST has been the standard for years—simple, cacheable, resource-oriented. GraphQL offers flexibility—clients ask for exactly what they need. In this lesson, you'll learn both approaches, when to use each, and best practices for API design.

1. Learning Objectives

By the end of this lesson, you will be able to:

  • Design RESTful APIs following best practices
  • Implement GraphQL APIs with queries and mutations
  • Choose between REST and GraphQL for your use case
  • Implement API versioning strategies
  • Add rate limiting and authentication
  • Document APIs with OpenAPI/Swagger

2. Why This Matters

Real-world scenario: Your mobile app needs user profile, recent orders, and product recommendations. REST: 3 separate requests or one bloated response. GraphQL: one request asking exactly for needed fields. The choice impacts performance, complexity, and developer experience.

3. Core Concepts

REST API Best Practices

REST Principles:
1. Resource-based (nouns, not verbs)
2. Use HTTP methods correctly
3. Use HTTP status codes
4. Stateless
5. Cacheable
6. HATEOAS (hypermedia links)

HTTP Methods:
┌────────────┬─────────────────────────────────────────────────────┐
│ Method     │ Purpose                                            │
├────────────┼─────────────────────────────────────────────────────┤
│ GET        │ Retrieve resource (safe, idempotent, cacheable)    │
│ POST       │ Create new resource (not idempotent)               │
│ PUT        │ Full update (idempotent)                          │
│ PATCH      │ Partial update (not necessarily idempotent)        │
│ DELETE     │ Remove resource (idempotent)                       │
└────────────┴─────────────────────────────────────────────────────┘

HTTP Status Codes:
┌────────────┬─────────────────────────────────────────────────────┐
│ Code       │ Meaning                                            │
├────────────┼─────────────────────────────────────────────────────┤
│ 200 OK     │ Success                                            │
│ 201 Created│ Resource created                                   │
│ 204 No Content│ Success, no body to return                      │
│ 304 Not Modified │ Use cached version                           │
├────────────┼─────────────────────────────────────────────────────┤
│ 400 Bad Request│ Client error (validation failed)               │
│ 401 Unauthorized │ Authentication required                      │
│ 403 Forbidden │ Authenticated but not authorized                │
│ 404 Not Found │ Resource doesn't exist                          │
│ 409 Conflict │ Duplicate or state conflict                      │
│ 429 Too Many Requests │ Rate limited                            │
├────────────┼─────────────────────────────────────────────────────┤
│ 500 Internal Server Error │ Server error                        │
│ 503 Service Unavailable │ Down for maintenance                  │
└────────────┴─────────────────────────────────────────────────────┘
REST API best practices
# Flask REST API example
from flask import Flask, request, jsonify, g
from functools import wraps
import time

app = Flask(__name__)

# Rate limiting
def rate_limit(limit=100, window=60):
    def decorator(f):
        @wraps(f)
        def wrapper(*args, **kwargs):
            # In production, use Redis for distributed rate limiting
            key = f"rate_limit:{request.remote_addr}"
            # Simplified: check if user has exceeded limit
            # return 429 if exceeded
            return f(*args, **kwargs)
        return wrapper
    return decorator

# REST endpoints
@app.route('/api/v1/users', methods=['GET'])
@rate_limit(limit=100)
def get_users():
    """GET /users - list users with pagination"""
    page = request.args.get('page', 1, type=int)
    per_page = request.args.get('per_page', 20, type=int)
    # Fetch from database
    users = db.get_users(page=page, per_page=per_page)
    return jsonify({
        'data': users,
        'pagination': {
            'page': page,
            'per_page': per_page,
            'total': db.user_count()
        }
    }), 200

@app.route('/api/v1/users/<int:user_id>', methods=['GET'])
def get_user(user_id):
    """GET /users/{id} - get single user"""
    user = db.get_user(user_id)
    if not user:
        return jsonify({'error': 'User not found'}), 404
    return jsonify({'data': user}), 200

@app.route('/api/v1/users', methods=['POST'])
def create_user():
    """POST /users - create user"""
    data = request.json
    # Validate required fields
    if not data.get('email'):
        return jsonify({'error': 'Email required'}), 400
    # Check for duplicate
    if db.user_exists(data['email']):
        return jsonify({'error': 'Email already exists'}), 409
    user = db.create_user(data)
    return jsonify({'data': user}), 201, {'Location': f'/users/{user["id"]}'}

@app.route('/api/v1/users/<int:user_id>', methods=['PUT'])
def update_user(user_id):
    """PUT /users/{id} - full update"""
    data = request.json
    user = db.update_user(user_id, data)
    if not user:
        return jsonify({'error': 'User not found'}), 404
    return jsonify({'data': user}), 200

@app.route('/api/v1/users/<int:user_id>', methods=['PATCH'])
def partial_update_user(user_id):
    """PATCH /users/{id} - partial update"""
    data = request.json
    user = db.partial_update_user(user_id, data)
    if not user:
        return jsonify({'error': 'User not found'}), 404
    return jsonify({'data': user}), 200

@app.route('/api/v1/users/<int:user_id>', methods=['DELETE'])
def delete_user(user_id):
    """DELETE /users/{id} - delete user"""
    deleted = db.delete_user(user_id)
    if not deleted:
        return jsonify({'error': 'User not found'}), 404
    return '', 204

# Nested resources
@app.route('/api/v1/users/<int:user_id>/orders', methods=['GET'])
def get_user_orders(user_id):
    """GET /users/{id}/orders - get user's orders"""
    orders = db.get_orders_by_user(user_id)
    return jsonify({'data': orders}), 200

# Filtering, sorting, field selection
@app.route('/api/v1/products', methods=['GET'])
def get_products():
    """GET /products?category=electronics&sort=-price&fields=id,name,price"""
    query = Product.query

    # Filtering
    if category := request.args.get('category'):
        query = query.filter_by(category=category)
    if min_price := request.args.get('min_price'):
        query = query.filter(Product.price >= float(min_price))

    # Sorting
    if sort := request.args.get('sort'):
        if sort.startswith('-'):
            query = query.order_by(desc(sort[1:]))
        else:
            query = query.order_by(asc(sort))

    # Field selection (projection)
    fields = request.args.get('fields', '').split(',')
    if fields:
        products = query.with_entities(*fields)

    products = query.paginate(page=page, per_page=per_page)
    return jsonify({'data': products.items, 'total': products.total}), 200
REST API implementation

GraphQL API

# Strawberry GraphQL example
import strawberry
from typing import List, Optional

# Define types
@strawberry.type
class Product:
    id: int
    name: str
    price: float
    category: str

@strawberry.type
class OrderItem:
    product: Product
    quantity: int
    price_at_time: float

@strawberry.type
class Order:
    id: int
    created_at: str
    status: str
    items: List[OrderItem]
    total: float

@strawberry.type
class User:
    id: int
    name: str
    email: str
    orders: List[Order]
    favorite_products: List[Product]

# Input types for mutations
@strawberry.input
class CreateUserInput:
    name: str
    email: str

@strawberry.input
class CreateOrderInput:
    product_id: int
    quantity: int

# Query resolver
@strawberry.type
class Query:
    @strawberry.field
    def user(self, id: int) -> Optional[User]:
        return db.get_user(id)

    @strawberry.field
    def users(self, limit: int = 10, offset: int = 0) -> List[User]:
        return db.get_users(limit=limit, offset=offset)

    @strawberry.field
    def product(self, id: int) -> Optional[Product]:
        return db.get_product(id)

    @strawberry.field
    def products(self, category: Optional[str] = None) -> List[Product]:
        if category:
            return db.get_products_by_category(category)
        return db.get_all_products()

    @strawberry.field
    def search_products(self, query: str) -> List[Product]:
        return db.search_products(query)

# Mutation resolvers
@strawberry.type
class Mutation:
    @strawberry.mutation
    def create_user(self, input: CreateUserInput) -> User:
        return db.create_user(input.name, input.email)

    @strawberry.mutation
    def create_order(self, user_id: int, input: CreateOrderInput) -> Order:
        return db.create_order(user_id, input.product_id, input.quantity)

    @strawberry.mutation
    def cancel_order(self, order_id: int) -> bool:
        return db.cancel_order(order_id)

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

# GraphQL query examples:
# Query with specific fields (clients ask for exactly what they need)
"""
query GetUserDashboard {
  user(id: 123) {
    name
    email
    orders(limit: 5) {
      id
      total
      status
      items {
        product {
          name
          price
        }
        quantity
      }
    }
  }
}
"""

# Mutation example:
"""
mutation CreateNewOrder {
  createOrder(userId: 123, input: {productId: 456, quantity: 2}) {
    id
    total
    status
  }
}
"""

# GraphQL advantages:
# - Client specifies exact fields needed (no over-fetching)
# - Single endpoint, multiple resources (no under-fetching)
# - Strong typing
# - Introspection (self-documenting)
# - Batching (N+1 problem solutions)

# GraphQL disadvantages:
# - Complexity (clients can write expensive queries)
# - Caching harder (single endpoint)
# - File uploads more complex
# - Real-time subscriptions add complexity
GraphQL API implementation with Strawberry

REST vs GraphQL Comparison

┌─────────────────────────────────────────────────────────────────┐
│              REST vs GraphQL Decision Guide                      │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  Use REST when:                                                  │
│  ─────────────                                                   │
│  ✓ Simple CRUD operations                                      │
│  ✓ Well-defined resources and relationships                    │
│  ✓ HTTP caching is important                                    │
│  ✓ Public API (CDN friendly)                                    │
│  ✓ Team is smaller or less experienced                          │
│  ✓ Clients are browsers (can cache GET requests)                │
│                                                                  │
│  Use GraphQL when:                                               │
│  ────────────────                                                │
│  ✓ Complex data graph (many relationships)                      │
│  ✓ Multiple clients with different needs (mobile + web)         │
│  ✓ Over-fetching is a problem (mobile bandwidth)                │
│  ✓ Under-fetching causes many round trips                       │
│  ✓ Rapid iteration (clients control what they need)             │
│  ✓ Real-time subscriptions required                             │
│                                                                  │
│  Hybrid approach:                                                │
│  ────────────────                                                │
│  • Public REST API + internal GraphQL                           │
│  • GraphQL gateway aggregating REST services                    │
│  • REST endpoints for simple CRUD + GraphQL for complex queries │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘
REST vs GraphQL decision guide

API Versioning Strategies

API Versioning Strategies:

1. URL Path Versioning (most common)
   ┌─────────────────────────────────────────────────────────────────┐
   │ https://api.example.com/v1/users                              │
   │ https://api.example.com/v2/users                              │
   │                                                                 │
   │ Pros: Simple, cacheable, easy to test                          │
   │ Cons: URI changes, long-term maintenance                       │
   └─────────────────────────────────────────────────────────────────┘

2. Query Parameter Versioning
   ┌─────────────────────────────────────────────────────────────────┐
   │ https://api.example.com/users?version=1                        │
   │ https://api.example.com/users?version=2                        │
   │                                                                 │
   │ Pros: Same URI                                                 │
   │ Cons: Not cacheable, hidden from URL                           │
   └─────────────────────────────────────────────────────────────────┘

3. Header Versioning
   ┌─────────────────────────────────────────────────────────────────┐
   │ Accept: application/vnd.example.v1+json                        │
   │ Accept: application/vnd.example.v2+json                        │
   │                                                                 │
   │ Pros: Clean URLs, version not visible                          │
   │ Cons: Harder to test, less discoverable                        │
   └─────────────────────────────────────────────────────────────────┘

4. Content Negotiation
   ┌─────────────────────────────────────────────────────────────────┐
   │ Accept: application/vnd.example.user.v1+json                   │
   │                                                                 │
   │ Pros: Very granular                                            │
   │ Cons: Complex, rarely used                                     │
   └─────────────────────────────────────────────────────────────────┘

Best Practice: URL Path Versioning + Deprecation Headers

Deprecation Strategy:
┌─────────────────────────────────────────────────────────────────┐
│ # Response headers for deprecated endpoints                     │
│ Deprecation: true                                               │
│ Sunset: Sun, 31 Dec 2025 23:59:59 GMT                          │
│ Link: <https://api.example.com/v2/users>; rel="successor-version"│
└─────────────────────────────────────────────────────────────────┘

Backward Compatible Changes (no version bump):
✓ Adding optional fields
✓ Adding new endpoints
✓ Widening validation (less strict)
✓ Adding new enum values

Breaking Changes (need version bump):
✗ Removing fields
✗ Changing field types
✗ Renaming fields
✗ Making optional fields required
✗ Changing validation (more strict)
✗ Changing error response format
API versioning strategies

4. Complete Project: API Gateway

# OpenAPI/Swagger documentation (api.yaml)
openapi: 3.0.0
info:
  title: E-Commerce API
  version: 1.0.0
  description: API for e-commerce platform

servers:
  - url: https://api.example.com/v1

paths:
  /users:
    get:
      summary: List users
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            default: 1
        - name: per_page
          in: query
          schema:
            type: integer
            default: 20
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/User'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
    post:
      summary: Create user
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - email
                - name
              properties:
                email:
                  type: string
                  format: email
                name:
                  type: string
      responses:
        '201':
          description: Created
        '409':
          description: Conflict (email exists)

components:
  schemas:
    User:
      type: object
      properties:
        id:
          type: integer
        email:
          type: string
        name:
          type: string
        created_at:
          type: string
          format: date-time

    Pagination:
      type: object
      properties:
        page:
          type: integer
        per_page:
          type: integer
        total:
          type: integer

# Generate server stub with:
# openapi-generator generate -i api.yaml -g python-flask -o ./server

# Generate client with:
# openapi-generator generate -i api.yaml -g python -o ./client
OpenAPI/Swagger documentation

5. Common Errors & Solutions

6. Summary Checklist

7. Next Steps

Next lesson: Backend Lesson 12.6: Microservices Architecture

Gataya Med

DevOps Engineer & Backend Developer. Sharing insights on cloud, automation, and scalable systems.

Comments (0)

Sarah Chen March 6, 2025

This is exactly what I needed! The initContainer approach solved our migration issues completely. Thanks for the detailed guide!

Reply

Leave a Comment