FastAPI vs Django - A Comprehensive Comparison
This guide provides a detailed comparison between FastAPI and Django, two popular Python backend frameworks, covering everything from simple concepts to advanced features with practical code examples.
Overview
| Feature | FastAPI | Django |
|---|---|---|
| Type | Modern async API framework | Full-stack web framework |
| Release Year | 2018 | 2005 |
| Primary Use Case | Building APIs (RESTful, GraphQL) | Full web applications with admin panel |
| Performance | Very High (async/await) | Moderate (WSGI, ASGI support added) |
| Learning Curve | Low to Medium | Medium to High |
| Built-in Admin | ❌ No | ✅ Yes |
| ORM | ❌ No (use SQLAlchemy, Tortoise) | ✅ Yes (Django ORM) |
| Async Support | ✅ Native | ⚠️ Partial (Django 3.1+) |
| Auto Documentation | ✅ Yes (OpenAPI/Swagger) | ❌ No (third-party packages) |
| Type Hints | ✅ Required (Pydantic) | ⚠️ Optional |
1. Basic Setup and Hello World
FastAPI
# main.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def hello_world():
return {"message": "Hello World"}
@app.get("/hello/{name}")
async def hello_name(name: str):
return {"message": f"Hello {name}"}
# Run with: uvicorn main:app --reload
Key Points:
- Minimal boilerplate
- Native async support with
async def - Automatic validation with type hints
- Built-in interactive API docs at
/docs
Django
# myproject/settings.py (partial)
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
# ... other apps
'myapp',
]
# myapp/views.py
from django.http import JsonResponse
def hello_world(request):
return JsonResponse({"message": "Hello World"})
def hello_name(request, name):
return JsonResponse({"message": f"Hello {name}"})
# myproject/urls.py
from django.urls import path
from myapp import views
urlpatterns = [
path('', views.hello_world),
path('hello/<str:name>/', views.hello_name),
]
# Run with: python manage.py runserver
Key Points:
- More project structure (settings, apps, urls)
- Includes admin interface, authentication out of the box
- Traditional synchronous request handling
- Better for full web applications with templates
2. Request Handling and Routing
FastAPI - Path Parameters and Query Strings
from fastapi import FastAPI, Query, Path
from typing import Optional
from enum import Enum
app = FastAPI()
class ModelType(str, Enum):
linear = "linear"
tree = "tree"
neural = "neural"
# Path parameters with validation
@app.get("/models/{model_id}")
async def get_model(
model_id: int = Path(..., ge=1, description="Model ID must be positive")
):
return {"model_id": model_id}
# Query parameters with defaults and validation
@app.get("/search")
async def search_models(
q: str = Query(..., min_length=3),
model_type: ModelType = ModelType.linear,
limit: int = Query(10, ge=1, le=100),
offset: int = 0
):
return {
"query": q,
"type": model_type,
"limit": limit,
"offset": offset
}
Django - Path and Query Parameters
# urls.py
from django.urls import path
from myapp import views
urlpatterns = [
path('models/<int:model_id>/', views.get_model),
path('search/', views.search_models),
]
# views.py
from django.http import JsonResponse
from django.core.exceptions import ValidationError
def get_model(request, model_id):
if model_id < 1:
return JsonResponse({"error": "Model ID must be positive"}, status=400)
return JsonResponse({"model_id": model_id})
def search_models(request):
q = request.GET.get('q', '')
if len(q) < 3:
return JsonResponse({"error": "Query must be at least 3 characters"}, status=400)
model_type = request.GET.get('model_type', 'linear')
limit = int(request.GET.get('limit', 10))
offset = int(request.GET.get('offset', 0))
# Manual validation
if limit < 1 or limit > 100:
return JsonResponse({"error": "Limit must be between 1 and 100"}, status=400)
return JsonResponse({
"query": q,
"type": model_type,
"limit": limit,
"offset": offset
})
FastAPI Advantages:
- Automatic validation with Pydantic
- Type hints provide editor support
- Validation errors are handled automatically
Django Advantages:
- More explicit control
- Built-in URL patterns with regex support
3. Request Body and Data Validation
FastAPI - Pydantic Models
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field, EmailStr, validator
from typing import List, Optional
from datetime import datetime
app = FastAPI()
class TrainingConfig(BaseModel):
learning_rate: float = Field(..., gt=0, le=1)
batch_size: int = Field(32, ge=1, le=1024)
epochs: int = Field(..., ge=1)
class ModelInput(BaseModel):
name: str = Field(..., min_length=3, max_length=100)
description: Optional[str] = None
tags: List[str] = []
config: TrainingConfig
created_at: datetime = Field(default_factory=datetime.now)
@validator('tags')
def validate_tags(cls, v):
if len(v) > 10:
raise ValueError('Maximum 10 tags allowed')
return v
@app.post("/models/")
async def create_model(model: ModelInput):
# Data is already validated
return {
"status": "created",
"model": model.dict()
}
# Automatic request/response validation
@app.get("/models/{model_id}", response_model=ModelInput)
async def get_model(model_id: int):
# Response is automatically validated against ModelInput
return {
"name": "MyModel",
"config": {
"learning_rate": 0.001,
"batch_size": 32,
"epochs": 100
}
}
Django - Forms and Serializers
# forms.py
from django import forms
from django.core.exceptions import ValidationError
class TrainingConfigForm(forms.Form):
learning_rate = forms.FloatField(min_value=0.0001, max_value=1.0)
batch_size = forms.IntegerField(min_value=1, max_value=1024)
epochs = forms.IntegerField(min_value=1)
class ModelForm(forms.Form):
name = forms.CharField(min_length=3, max_length=100)
description = forms.CharField(required=False)
tags = forms.JSONField(required=False)
def clean_tags(self):
tags = self.cleaned_data.get('tags', [])
if len(tags) > 10:
raise ValidationError('Maximum 10 tags allowed')
return tags
# views.py
import json
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
@csrf_exempt
@require_http_methods(["POST"])
def create_model(request):
try:
data = json.loads(request.body)
except json.JSONDecodeError:
return JsonResponse({"error": "Invalid JSON"}, status=400)
form = ModelForm(data)
if form.is_valid():
return JsonResponse({
"status": "created",
"model": form.cleaned_data
})
else:
return JsonResponse({"errors": form.errors}, status=400)
# Alternative: Using Django REST Framework
from rest_framework import serializers, views, status
from rest_framework.response import Response
class TrainingConfigSerializer(serializers.Serializer):
learning_rate = serializers.FloatField(min_value=0.0001, max_value=1.0)
batch_size = serializers.IntegerField(min_value=1, max_value=1024)
epochs = serializers.IntegerField(min_value=1)
class ModelSerializer(serializers.Serializer):
name = serializers.CharField(min_length=3, max_length=100)
description = serializers.CharField(required=False, allow_blank=True)
tags = serializers.ListField(child=serializers.CharField(), required=False)
config = TrainingConfigSerializer()
def validate_tags(self, value):
if len(value) > 10:
raise serializers.ValidationError('Maximum 10 tags allowed')
return value
class ModelCreateView(views.APIView):
def post(self, request):
serializer = ModelSerializer(data=request.data)
if serializer.is_valid():
return Response({
"status": "created",
"model": serializer.validated_data
}, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
FastAPI Advantages:
- Pydantic models are simpler and more intuitive
- Automatic OpenAPI documentation from models
- Better type safety and IDE support
Django Advantages:
- Django REST Framework is battle-tested
- More flexible for complex validation logic
- Tight integration with Django ORM
4. Database Integration
FastAPI - SQLAlchemy
from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy import create_engine, Column, Integer, String, Float, DateTime
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session
from pydantic import BaseModel
from datetime import datetime
from typing import List
# Database setup
DATABASE_URL = "sqlite:///./models.db"
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
# SQLAlchemy Model
class ModelDB(Base):
__tablename__ = "models"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, unique=True, index=True)
accuracy = Column(Float)
created_at = Column(DateTime, default=datetime.utcnow)
Base.metadata.create_all(bind=engine)
# Pydantic Schemas
class ModelCreate(BaseModel):
name: str
accuracy: float
class ModelResponse(BaseModel):
id: int
name: str
accuracy: float
created_at: datetime
class Config:
from_attributes = True # Formerly orm_mode
# Dependency
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
app = FastAPI()
@app.post("/models/", response_model=ModelResponse)
async def create_model(model: ModelCreate, db: Session = Depends(get_db)):
db_model = ModelDB(**model.dict())
db.add(db_model)
db.commit()
db.refresh(db_model)
return db_model
@app.get("/models/", response_model=List[ModelResponse])
async def list_models(skip: int = 0, limit: int = 10, db: Session = Depends(get_db)):
models = db.query(ModelDB).offset(skip).limit(limit).all()
return models
@app.get("/models/{model_id}", response_model=ModelResponse)
async def get_model(model_id: int, db: Session = Depends(get_db)):
model = db.query(ModelDB).filter(ModelDB.id == model_id).first()
if model is None:
raise HTTPException(status_code=404, detail="Model not found")
return model
Django - Django ORM
# models.py
from django.db import models
class Model(models.Model):
name = models.CharField(max_length=200, unique=True, db_index=True)
accuracy = models.FloatField()
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['-created_at']
def __str__(self):
return self.name
# serializers.py (Django REST Framework)
from rest_framework import serializers
from .models import Model
class ModelSerializer(serializers.ModelSerializer):
class Meta:
model = Model
fields = ['id', 'name', 'accuracy', 'created_at']
read_only_fields = ['id', 'created_at']
# views.py
from rest_framework import viewsets, status
from rest_framework.response import Response
from .models import Model
from .serializers import ModelSerializer
class ModelViewSet(viewsets.ModelViewSet):
queryset = Model.objects.all()
serializer_class = ModelSerializer
def create(self, request):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
# urls.py
from rest_framework.routers import DefaultRouter
from .views import ModelViewSet
router = DefaultRouter()
router.register(r'models', ModelViewSet)
urlpatterns = router.urls
# Alternative: Function-based views
from django.http import JsonResponse
from django.views.decorators.http import require_http_methods
from .models import Model
@require_http_methods(["GET"])
def list_models(request):
skip = int(request.GET.get('skip', 0))
limit = int(request.GET.get('limit', 10))
models = Model.objects.all()[skip:skip+limit]
data = [
{
'id': m.id,
'name': m.name,
'accuracy': m.accuracy,
'created_at': m.created_at.isoformat()
}
for m in models
]
return JsonResponse(data, safe=False)
FastAPI Advantages:
- Flexible ORM choice (SQLAlchemy, Tortoise, etc.)
- Explicit dependency injection
- Better for microservices architecture
Django Advantages:
- Powerful built-in ORM with migrations
- Admin interface for free
- Better for monolithic applications
- Rich ecosystem of database backends
5. Authentication and Authorization
FastAPI - JWT Authentication
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from pydantic import BaseModel
from datetime import datetime, timedelta
from typing import Optional
import jwt
from passlib.context import CryptContext
app = FastAPI()
security = HTTPBearer()
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
SECRET_KEY = "your-secret-key"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
# Pydantic models
class User(BaseModel):
username: str
email: str
full_name: Optional[str] = None
class UserInDB(User):
hashed_password: str
class Token(BaseModel):
access_token: str
token_type: str
class LoginRequest(BaseModel):
username: str
password: str
# Mock database
fake_users_db = {
"john": {
"username": "john",
"email": "john@example.com",
"full_name": "John Doe",
"hashed_password": pwd_context.hash("secret123")
}
}
def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_password)
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
to_encode = data.copy()
expire = datetime.utcnow() + (expires_delta or timedelta(minutes=15))
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)):
token = credentials.credentials
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials"
)
except jwt.PyJWTError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials"
)
user = fake_users_db.get(username)
if user is None:
raise HTTPException(status_code=404, detail="User not found")
return User(**user)
@app.post("/token", response_model=Token)
async def login(login_data: LoginRequest):
user = fake_users_db.get(login_data.username)
if not user or not verify_password(login_data.password, user["hashed_password"]):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password"
)
access_token = create_access_token(
data={"sub": login_data.username},
expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
)
return {"access_token": access_token, "token_type": "bearer"}
@app.get("/users/me", response_model=User)
async def read_users_me(current_user: User = Depends(get_current_user)):
return current_user
@app.get("/protected")
async def protected_route(current_user: User = Depends(get_current_user)):
return {"message": f"Hello {current_user.username}, this is protected!"}
Django - Built-in Authentication
# settings.py
INSTALLED_APPS = [
'django.contrib.auth',
'django.contrib.contenttypes',
'rest_framework',
'rest_framework.authtoken', # For token auth
]
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
}
# models.py
from django.contrib.auth.models import AbstractUser
class CustomUser(AbstractUser):
# Add custom fields if needed
pass
# serializers.py
from rest_framework import serializers
from django.contrib.auth import get_user_model
User = get_user_model()
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'email', 'first_name', 'last_name']
read_only_fields = ['id']
class LoginSerializer(serializers.Serializer):
username = serializers.CharField()
password = serializers.CharField(write_only=True)
# views.py
from rest_framework import status, viewsets
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response
from rest_framework.authtoken.models import Token
from django.contrib.auth import authenticate
@api_view(['POST'])
@permission_classes([AllowAny])
def login(request):
serializer = LoginSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = authenticate(
username=serializer.validated_data['username'],
password=serializer.validated_data['password']
)
if user is None:
return Response(
{"detail": "Incorrect username or password"},
status=status.HTTP_401_UNAUTHORIZED
)
token, created = Token.objects.get_or_create(user=user)
return Response({
"access_token": token.key,
"token_type": "Token"
})
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def get_current_user(request):
serializer = UserSerializer(request.user)
return Response(serializer.data)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def protected_route(request):
return Response({
"message": f"Hello {request.user.username}, this is protected!"
})
# urls.py
from django.urls import path
from . import views
urlpatterns = [
path('token/', views.login),
path('users/me/', views.get_current_user),
path('protected/', views.protected_route),
]
FastAPI Advantages:
- Flexible authentication strategies
- Easy to implement custom auth
- Better for stateless JWT authentication
Django Advantages:
- Built-in user management system
- Session-based auth out of the box
- Comprehensive permission system
- Admin interface for user management
6. Middleware and Request Lifecycle
FastAPI - Middleware
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware
import time
import logging
app = FastAPI()
# CORS Middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# GZip Middleware
app.add_middleware(GZipMiddleware, minimum_size=1000)
# Custom Middleware - Timing
@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
start_time = time.time()
response = await call_next(request)
process_time = time.time() - start_time
response.headers["X-Process-Time"] = str(process_time)
return response
# Custom Middleware - Logging
@app.middleware("http")
async def log_requests(request: Request, call_next):
logging.info(f"Request: {request.method} {request.url}")
response = await call_next(request)
logging.info(f"Response status: {response.status_code}")
return response
# Custom Middleware - Authentication check
@app.middleware("http")
async def authenticate_request(request: Request, call_next):
# Skip auth for public endpoints
if request.url.path in ["/docs", "/openapi.json", "/login"]:
return await call_next(request)
# Check for API key
api_key = request.headers.get("X-API-Key")
if not api_key or api_key != "secret-key":
return Response(
content="Invalid API Key",
status_code=401
)
response = await call_next(request)
return response
@app.get("/")
async def root():
return {"message": "Hello World"}
Django - Middleware
# middleware.py
import time
import logging
from django.utils.deprecation import MiddlewareMixin
from django.http import JsonResponse
logger = logging.getLogger(__name__)
class ProcessTimeMiddleware(MiddlewareMixin):
def process_request(self, request):
request.start_time = time.time()
def process_response(self, request, response):
if hasattr(request, 'start_time'):
process_time = time.time() - request.start_time
response['X-Process-Time'] = str(process_time)
return response
class LoggingMiddleware(MiddlewareMixin):
def process_request(self, request):
logger.info(f"Request: {request.method} {request.path}")
def process_response(self, request, response):
logger.info(f"Response status: {response.status_code}")
return response
class APIKeyAuthMiddleware(MiddlewareMixin):
def process_request(self, request):
# Skip auth for public endpoints
public_paths = ['/admin/', '/login/']
if any(request.path.startswith(path) for path in public_paths):
return None
api_key = request.META.get('HTTP_X_API_KEY')
if not api_key or api_key != 'secret-key':
return JsonResponse(
{"error": "Invalid API Key"},
status=401
)
return None
# settings.py
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'myapp.middleware.ProcessTimeMiddleware',
'myapp.middleware.LoggingMiddleware',
'myapp.middleware.APIKeyAuthMiddleware',
]
# CORS settings
INSTALLED_APPS += ['corsheaders']
MIDDLEWARE = ['corsheaders.middleware.CorsMiddleware'] + MIDDLEWARE
CORS_ALLOW_ALL_ORIGINS = True
FastAPI Advantages:
- Simpler middleware syntax
- Async middleware support
- Easy to add/remove middleware
Django Advantages:
- More mature middleware ecosystem
- Built-in security middleware
- Extensive configuration options
7. Dependency Injection
FastAPI - Built-in Dependency Injection
from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.orm import Session
from typing import Optional
app = FastAPI()
# Database dependency
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
# Configuration dependency
class Settings:
def __init__(self):
self.api_version = "v1"
self.debug = True
def get_settings():
return Settings()
# Authentication dependency
def get_current_user(token: str = Header(...)):
# Validate token
user = validate_token(token)
if not user:
raise HTTPException(status_code=401, detail="Invalid token")
return user
# Permission dependency
def require_admin(current_user: User = Depends(get_current_user)):
if not current_user.is_admin:
raise HTTPException(status_code=403, detail="Admin access required")
return current_user
# Pagination dependency
class Pagination:
def __init__(self, skip: int = 0, limit: int = 100):
self.skip = skip
self.limit = limit
# Using dependencies
@app.get("/models/")
async def list_models(
db: Session = Depends(get_db),
pagination: Pagination = Depends(),
settings: Settings = Depends(get_settings)
):
models = db.query(Model).offset(pagination.skip).limit(pagination.limit).all()
return {
"models": models,
"version": settings.api_version
}
@app.post("/admin/models/")
async def create_admin_model(
model_data: ModelCreate,
db: Session = Depends(get_db),
admin: User = Depends(require_admin)
):
# Only admins can create models
model = Model(**model_data.dict(), created_by=admin.id)
db.add(model)
db.commit()
return model
# Dependency with sub-dependencies
def get_query_params(
q: Optional[str] = None,
pagination: Pagination = Depends()
):
return {"query": q, "pagination": pagination}
@app.get("/search/")
async def search(params: dict = Depends(get_query_params)):
return params
Django - Dependency Management
# Django doesn't have built-in DI, but we can use patterns
# utils.py
from functools import wraps
from django.http import JsonResponse
def inject_db_connection(view_func):
"""Decorator to inject database connection"""
@wraps(view_func)
def wrapper(request, *args, **kwargs):
from django.db import connection
kwargs['db'] = connection
return view_func(request, *args, **kwargs)
return wrapper
def require_admin(view_func):
"""Decorator to require admin access"""
@wraps(view_func)
def wrapper(request, *args, **kwargs):
if not request.user.is_authenticated or not request.user.is_staff:
return JsonResponse(
{"error": "Admin access required"},
status=403
)
return view_func(request, *args, **kwargs)
return wrapper
# views.py
from django.views.decorators.http import require_http_methods
from .models import Model
from .utils import inject_db_connection, require_admin
@require_http_methods(["GET"])
@inject_db_connection
def list_models(request, db=None):
skip = int(request.GET.get('skip', 0))
limit = int(request.GET.get('limit', 100))
models = Model.objects.all()[skip:skip+limit]
return JsonResponse([m.to_dict() for m in models], safe=False)
@require_http_methods(["POST"])
@require_admin
def create_admin_model(request):
# Only admins can create models
# Implementation here
pass
# Alternative: Django REST Framework with custom permissions
from rest_framework import viewsets, permissions
from rest_framework.decorators import action
class IsAdminUser(permissions.BasePermission):
def has_permission(self, request, view):
return request.user and request.user.is_staff
class ModelViewSet(viewsets.ModelViewSet):
queryset = Model.objects.all()
serializer_class = ModelSerializer
def get_permissions(self):
if self.action == 'create':
return [IsAdminUser()]
return [permissions.IsAuthenticated()]
@action(detail=False, methods=['get'])
def recent(self, request):
skip = int(request.query_params.get('skip', 0))
limit = int(request.query_params.get('limit', 100))
models = self.queryset.all()[skip:skip+limit]
serializer = self.get_serializer(models, many=True)
return Response(serializer.data)
FastAPI Advantages:
- Native dependency injection system
- Automatic dependency resolution
- Cleaner and more testable code
- Better separation of concerns
Django Advantages:
- Decorators provide similar functionality
- Class-based views with mixins
- DRF provides comprehensive view logic
8. Async/Await Support
FastAPI - Native Async
from fastapi import FastAPI
import asyncio
import httpx
from typing import List
app = FastAPI()
# Async database query (with async ORM like Tortoise)
@app.get("/models/{model_id}")
async def get_model(model_id: int):
# Async database query
model = await ModelDB.get(id=model_id)
return model
# Concurrent API calls
@app.get("/aggregate-data")
async def aggregate_data():
async with httpx.AsyncClient() as client:
# Run multiple API calls concurrently
tasks = [
client.get("https://api1.example.com/data"),
client.get("https://api2.example.com/data"),
client.get("https://api3.example.com/data"),
]
responses = await asyncio.gather(*tasks)
return {
"data": [r.json() for r in responses]
}
# Async background processing
@app.post("/process-batch")
async def process_batch(items: List[str]):
async def process_item(item: str):
await asyncio.sleep(1) # Simulate async work
return f"Processed: {item}"
results = await asyncio.gather(*[process_item(item) for item in items])
return {"results": results}
# Mixed sync/async (FastAPI handles both)
@app.get("/sync-operation")
def sync_operation():
# This is still fine, FastAPI will run it in a thread pool
import time
time.sleep(1)
return {"message": "Done"}
Django - Async Support (Django 3.1+)
# views.py
from django.http import JsonResponse
import asyncio
import httpx
# Async view
async def get_model(request, model_id):
# Async ORM query (Django 4.1+)
from .models import Model
model = await Model.objects.aget(id=model_id)
return JsonResponse({
"id": model.id,
"name": model.name
})
# Async with external API calls
async def aggregate_data(request):
async with httpx.AsyncClient() as client:
tasks = [
client.get("https://api1.example.com/data"),
client.get("https://api2.example.com/data"),
client.get("https://api3.example.com/data"),
]
responses = await asyncio.gather(*tasks)
return JsonResponse({
"data": [r.json() for r in responses]
})
# urls.py
from django.urls import path
from . import views
urlpatterns = [
path('models/<int:model_id>/', views.get_model),
path('aggregate/', views.aggregate_data),
]
# settings.py
# For async support, use ASGI server
ASGI_APPLICATION = 'myproject.asgi.application'
# asgi.py
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
application = get_asgi_application()
# Run with: uvicorn myproject.asgi:application --reload
FastAPI Advantages:
- Built from the ground up for async
- Better async performance
- Easier to write async code
- Full async ecosystem support
Django Advantages:
- Can still use sync code
- Gradual migration to async
- Extensive sync ecosystem
9. Testing
FastAPI - Testing with TestClient
from fastapi.testclient import TestClient
from main import app
import pytest
client = TestClient(app)
def test_read_main():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello World"}
def test_create_model():
response = client.post(
"/models/",
json={
"name": "TestModel",
"accuracy": 0.95,
"config": {
"learning_rate": 0.001,
"batch_size": 32,
"epochs": 100
}
}
)
assert response.status_code == 200
data = response.json()
assert data["name"] == "TestModel"
assert data["accuracy"] == 0.95
def test_authentication():
# Test without token
response = client.get("/protected")
assert response.status_code == 401
# Get token
response = client.post(
"/token",
json={"username": "john", "password": "secret123"}
)
token = response.json()["access_token"]
# Test with token
response = client.get(
"/protected",
headers={"Authorization": f"Bearer {token}"}
)
assert response.status_code == 200
# Async testing
@pytest.mark.asyncio
async def test_async_endpoint():
from httpx import AsyncClient
async with AsyncClient(app=app, base_url="http://test") as ac:
response = await ac.get("/")
assert response.status_code == 200
# Dependency override for testing
def override_get_db():
# Return test database
pass
app.dependency_overrides[get_db] = override_get_db
Django - Testing
from django.test import TestCase, Client
from rest_framework.test import APITestCase, APIClient
from django.contrib.auth import get_user_model
from .models import Model
User = get_user_model()
class ModelAPITestCase(APITestCase):
def setUp(self):
self.client = APIClient()
self.user = User.objects.create_user(
username='testuser',
password='testpass123'
)
def test_list_models(self):
response = self.client.get('/api/models/')
self.assertEqual(response.status_code, 200)
def test_create_model(self):
self.client.force_authenticate(user=self.user)
data = {
'name': 'TestModel',
'accuracy': 0.95
}
response = self.client.post('/api/models/', data)
self.assertEqual(response.status_code, 201)
self.assertEqual(Model.objects.count(), 1)
self.assertEqual(Model.objects.first().name, 'TestModel')
def test_authentication_required(self):
response = self.client.get('/api/protected/')
self.assertEqual(response.status_code, 401)
self.client.force_authenticate(user=self.user)
response = self.client.get('/api/protected/')
self.assertEqual(response.status_code, 200)
class ModelTestCase(TestCase):
def test_model_creation(self):
model = Model.objects.create(
name='TestModel',
accuracy=0.95
)
self.assertEqual(model.name, 'TestModel')
self.assertEqual(model.accuracy, 0.95)
self.assertIsNotNone(model.created_at)
# Run tests with: python manage.py test
FastAPI Advantages:
- Simpler test setup
- Built-in TestClient
- Easy dependency overrides
Django Advantages:
- Comprehensive test framework
- Database transaction handling
- Test fixtures and factories
- Better test isolation
10. Performance Comparison
Benchmark Results (Approximate)
| Metric | FastAPI | Django (WSGI) | Django (ASGI) |
|---|---|---|---|
| Requests/sec | ~20,000 | ~5,000 | ~15,000 |
| Latency (ms) | ~5ms | ~20ms | ~8ms |
| Concurrency | Excellent | Good | Very Good |
| Memory Usage | Low | Medium | Medium |
FastAPI - Performance Optimizations
from fastapi import FastAPI
from fastapi.responses import ORJSONResponse # Faster JSON
from functools import lru_cache
app = FastAPI(default_response_class=ORJSONResponse)
# Caching expensive computations
@lru_cache(maxsize=128)
def expensive_computation(param: int):
# Expensive operation
return result
@app.get("/compute/{value}")
async def compute(value: int):
result = expensive_computation(value)
return {"result": result}
# Background tasks
from fastapi import BackgroundTasks
def write_log(message: str):
with open("log.txt", "a") as f:
f.write(message + "\n")
@app.post("/send-notification")
async def send_notification(
email: str,
background_tasks: BackgroundTasks
):
background_tasks.add_task(write_log, f"Sent email to {email}")
return {"message": "Email queued"}
Django - Performance Optimizations
from django.views.decorators.cache import cache_page
from django.core.cache import cache
from django.db import connection
from django.db.models import Prefetch, select_related, prefetch_related
# View caching
@cache_page(60 * 15) # Cache for 15 minutes
def cached_view(request):
# Expensive view
return JsonResponse({"data": expensive_operation()})
# Query optimization
def optimized_query():
# Use select_related for ForeignKey
models = Model.objects.select_related('user').all()
# Use prefetch_related for ManyToMany
models = Model.objects.prefetch_related('tags').all()
# Only fetch needed fields
models = Model.objects.only('id', 'name').all()
return models
# Database connection pooling (settings.py)
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'CONN_MAX_AGE': 600, # Connection pooling
}
}
# Use cache for expensive operations
def get_expensive_data(key):
data = cache.get(key)
if data is None:
data = expensive_operation()
cache.set(key, data, 3600) # Cache for 1 hour
return data
11. When to Choose Which Framework
Choose FastAPI When:
✅ Building RESTful APIs or microservices ✅ Need high performance and async support ✅ Want automatic API documentation ✅ Working with modern Python (type hints) ✅ Need WebSocket support ✅ Building machine learning model APIs ✅ Want lightweight and fast development ✅ Need to integrate with async libraries
Choose Django When:
✅ Building full-stack web applications ✅ Need built-in admin interface ✅ Want comprehensive user authentication ✅ Building content management systems ✅ Need mature ORM with migrations ✅ Want battle-tested security features ✅ Building monolithic applications ✅ Need extensive third-party packages ✅ Want rapid development with scaffolding
12. Hybrid Approach
You can even combine both frameworks:
# Use Django for admin and database
# Use FastAPI for high-performance API endpoints
# Django models and admin
from django.db import models
class Model(models.Model):
name = models.CharField(max_length=200)
accuracy = models.FloatField()
# FastAPI for API endpoints
from fastapi import FastAPI
import django
import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
django.setup()
from myapp.models import Model as DjangoModel
app = FastAPI()
@app.get("/api/models/")
async def list_models():
# Use Django ORM in FastAPI
models = await sync_to_async(list)(
DjangoModel.objects.all()
)
return models
Conclusion
Both FastAPI and Django are excellent frameworks with different strengths:
FastAPI excels at:
- High-performance APIs
- Modern Python features
- Async operations
- Automatic documentation
Django excels at:
- Full-stack web applications
- Rapid development
- Built-in features (admin, auth, ORM)
- Mature ecosystem
Choose based on your project requirements, team expertise, and specific use case. For pure API development, FastAPI is often the better choice. For complete web applications with admin interfaces and complex authentication, Django might be more suitable.