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
Comments (0)
This is exactly what I needed! The initContainer approach solved our migration issues completely. Thanks for the detailed guide!
ReplyLeave a Comment