FastAPI vs Django vs Flask: Architecture, Performance, async
Choosing the right Python web framework
Interview Question
"Compare FastAPI, Django, and Flask. What are the architectural differences? When would you use each? How do they handle async, performance, and scalability?"
Difficulty: Medium | Frequently asked at Google, Meta, Amazon
Theoretical Foundation
Framework Overview
# Flask: Micro-framework, minimal, flexible
from flask import Flask, jsonify
app = Flask(__name__)
@app.route('/api/users')
def get_users():
return jsonify([{"id": 1, "name": "Alice"}])
# Django: Full-featured, batteries included
from django.http import JsonResponse
def get_users(request):
return JsonResponse([{"id": 1, "name": "Alice"}], safe=False)
# FastAPI: Modern, async, automatic docs
from fastapi import FastAPI
app = FastAPI()
@app.get("/api/users")
async def get_users():
return [{"id": 1, "name": "Alice"}]
ℹ️
Key Concept: Each framework has different philosophies: Flask (minimal), Django (batteries included), FastAPI (modern async).
Flask
Basic Flask Application
from flask import Flask, request, jsonify, g
from functools import wraps
import time
app = Flask(__name__)
# Middleware
@app.before_request
def before_request():
g.start_time = time.time()
@app.after_request
def after_request(response):
if hasattr(g, 'start_time'):
elapsed = time.time() - g.start_time
response.headers['X-Response-Time'] = f'{elapsed:.3f}s'
return response
# Routes
@app.route('/api/users', methods=['GET'])
def get_users():
"""Get all users."""
users = [
{"id": 1, "name": "Alice", "email": "alice@example.com"},
{"id": 2, "name": "Bob", "email": "bob@example.com"}
]
return jsonify(users)
@app.route('/api/users/<int:user_id>', methods=['GET'])
def get_user(user_id):
"""Get user by ID."""
user = {"id": user_id, "name": "Alice"}
return jsonify(user)
@app.route('/api/users', methods=['POST'])
def create_user():
"""Create a new user."""
data = request.get_json()
if not data or 'name' not in data:
return jsonify({"error": "Name required"}), 400
user = {"id": 1, **data}
return jsonify(user), 201
# Error handlers
@app.errorhandler(404)
def not_found(error):
return jsonify({"error": "Not found"}), 404
@app.errorhandler(500)
def server_error(error):
return jsonify({"error": "Internal server error"}), 500
# Run
if __name__ == '__main__':
app.run(debug=True)
Flask with Extensions
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_marshmallow import Marshmallow
from flask_restful import Api, Resource
from flask_caching import Cache
app = Flask(__name__)
# Configuration
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db'
app.config['CACHE_TYPE'] = 'simple'
# Initialize extensions
db = SQLAlchemy(app)
ma = Marshmallow(app)
api = Api(app)
cache = Cache(app)
# Models
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
email = db.Column(db.String(100), unique=True, nullable=False)
# Schemas
class UserSchema(ma.SQLAlchemyAutoSchema):
class Meta:
model = User
# RESTful API
class UserResource(Resource):
@cache.cached(timeout=60)
def get(self, user_id):
user = User.query.get_or_404(user_id)
return UserSchema().dump(user)
def put(self, user_id):
user = User.query.get_or_404(user_id)
data = request.get_json()
user.name = data.get('name', user.name)
db.session.commit()
return UserSchema().dump(user)
api.add_resource(UserResource, '/api/users/<int:user_id>')
# Run
if __name__ == '__main__':
with app.app_context():
db.create_all()
app.run(debug=True)
💡
Interview Tip: Flask is great for microservices and small APIs, but requires more setup for large applications.
Django
Basic Django Application
# views.py
from django.http import JsonResponse
from django.views.decorators.http import require_http_methods
from django.views.decorators.csrf import csrf_exempt
from django.shortcuts import get_object_or_404
import json
# Models
from django.db import models
class User(models.Model):
name = models.CharField(max_length=100)
email = models.EmailField(unique=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['-created_at']
# Serializers
from rest_framework import serializers
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'name', 'email', 'created_at']
read_only_fields = ['id', 'created_at']
# Views (Function-based)
@csrf_exempt
@require_http_methods(["GET", "POST"])
def user_list(request):
if request.method == "GET":
users = User.objects.all()
serializer = UserSerializer(users, many=True)
return JsonResponse(serializer.data, safe=False)
elif request.method == "POST":
data = json.loads(request.body)
serializer = UserSerializer(data=data)
if serializer.is_valid():
serializer.save()
return JsonResponse(serializer.data, status=201)
return JsonResponse(serializer.errors, status=400)
# Views (Class-based)
from django.views import View
from django.utils.decorators import method_decorator
@method_decorator(csrf_exempt, name='dispatch')
class UserViewSet(View):
def get(self, request, user_id=None):
if user_id:
user = get_object_or_404(User, id=user_id)
return JsonResponse(UserSerializer(user).data)
users = User.objects.all()
return JsonResponse(UserSerializer(users, many=True).data, safe=False)
def post(self, request):
data = json.loads(request.body)
serializer = UserSerializer(data=data)
if serializer.is_valid():
serializer.save()
return JsonResponse(serializer.data, status=201)
return JsonResponse(serializer.errors, status=400)
# URLs
from django.urls import path
urlpatterns = [
path('api/users/', user_list),
path('api/users/<int:user_id>/', UserViewSet.as_view()),
]
Django REST Framework
# serializers.py
from rest_framework import serializers
from .models import User
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'name', 'email', 'created_at']
def validate_email(self, value):
if User.objects.filter(email=value).exists():
raise serializers.ValidationError("Email already exists")
return value
# views.py
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from django_filters.rest_framework import DjangoFilterBackend
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['name', 'email']
@action(detail=False, methods=['get'])
def active(self, request):
users = self.queryset.filter(is_active=True)
serializer = self.get_serializer(users, many=True)
return Response(serializer.data)
@action(detail=True, methods=['post'])
def deactivate(self, request, pk=None):
user = self.get_object()
user.is_active = False
user.save()
return Response({'status': 'deactivated'})
# urls.py
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register(r'users', UserViewSet)
urlpatterns = router.urls
ℹ️
Django Advantage: Built-in admin, ORM, authentication, and security features save development time.
FastAPI
Basic FastAPI Application
from fastapi import FastAPI, HTTPException, Depends, Query, Path
from pydantic import BaseModel, EmailStr, Field
from typing import List, Optional
from datetime import datetime
import uvicorn
app = FastAPI(title="User API", version="1.0.0")
# Pydantic models
class UserBase(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
email: EmailStr
class UserCreate(UserBase):
pass
class UserResponse(UserBase):
id: int
created_at: datetime
class Config:
from_attributes = True
# Dependency injection
async def get_db():
"""Database dependency."""
db = {"connection": True}
try:
yield db
finally:
db["connection"] = False
async def get_current_user(token: str = Header(...)):
"""Auth dependency."""
if token != "valid_token":
raise HTTPException(status_code=401, detail="Invalid token")
return {"id": 1, "name": "Alice"}
# Routes
@app.get("/api/users", response_model=List[UserResponse])
async def get_users(
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=1000),
db: dict = Depends(get_db)
):
"""Get all users with pagination."""
users = [
{"id": 1, "name": "Alice", "email": "alice@example.com", "created_at": datetime.now()},
{"id": 2, "name": "Bob", "email": "bob@example.com", "created_at": datetime.now()}
]
return users[skip:skip+limit]
@app.get("/api/users/{user_id}", response_model=UserResponse)
async def get_user(
user_id: int = Path(..., gt=0),
current_user: dict = Depends(get_current_user)
):
"""Get user by ID."""
if user_id == 999:
raise HTTPException(status_code=404, detail="User not found")
return {"id": user_id, "name": "Alice", "email": "alice@example.com", "created_at": datetime.now()}
@app.post("/api/users", response_model=UserResponse, status_code=201)
async def create_user(user: UserCreate, db: dict = Depends(get_db)):
"""Create a new user."""
return {"id": 1, **user.dict(), "created_at": datetime.now()}
# Error handling
@app.exception_handler(HTTPException)
async def http_exception_handler(request, exc):
return JSONResponse(
status_code=exc.status_code,
content={"detail": exc.detail}
)
# Run
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
FastAPI with Async
from fastapi import FastAPI
from typing import AsyncGenerator
import asyncio
import httpx
app = FastAPI()
# Async database operations
async def async_fetch_users():
"""Simulate async database fetch."""
await asyncio.sleep(0.1) # Simulate I/O
return [
{"id": 1, "name": "Alice"},
{"id": 2, "name": "Bob"}
]
# Async HTTP client
async def fetch_external_api(url: str) -> dict:
"""Fetch from external API."""
async with httpx.AsyncClient() as client:
response = await client.get(url)
return response.json()
# Async generator for streaming
async def stream_users() -> AsyncGenerator[dict, None]:
"""Stream users one by one."""
for i in range(10):
await asyncio.sleep(0.1)
yield {"id": i, "name": f"User_{i}"}
@app.get("/api/users/stream")
async def stream_users_endpoint():
"""Stream users endpoint."""
return StreamingResponse(
stream_users(),
media_type="application/json"
)
@app.get("/api/users/async")
async def get_users_async():
"""Async endpoint."""
users = await async_fetch_users()
return users
@app.get("/api/users/{user_id}/external")
async def get_user_external(user_id: int):
"""Fetch user from external API."""
data = await fetch_external_api(f"https://api.example.com/users/{user_id}")
return data
FastAPI Background Tasks
from fastapi import FastAPI, BackgroundTasks
from typing import Optional
import asyncio
app = FastAPI()
# Background task function
def write_log(message: str):
"""Write to log file."""
with open("app.log", "a") as f:
f.write(f"{message}\n")
async def process_data(data: dict):
"""Process data asynchronously."""
await asyncio.sleep(1) # Simulate processing
print(f"Processed: {data}")
@app.post("/api/users")
async def create_user(user: dict, background_tasks: BackgroundTasks):
"""Create user with background processing."""
# Add background tasks
background_tasks.add_task(write_log, f"Created user: {user}")
background_tasks.add_task(process_data, user)
return {"status": "created", "user": user}
@app.on_event("startup")
async def startup():
"""Startup event."""
print("Starting up...")
@app.on_event("shutdown")
async def shutdown():
"""Shutdown event."""
print("Shutting down...")
💡
FastAPI Advantage: Automatic API docs, type validation, and native async support make it ideal for modern APIs.
Comparison
Performance Benchmark
# Benchmark results (approximate)
# Requests per second (higher is better)
benchmarks = {
"Flask": {"requests_sec": 1000, "latency_ms": 5.0},
"Django": {"requests_sec": 800, "latency_ms": 6.5},
"FastAPI": {"requests_sec": 3000, "latency_ms": 1.5},
}
print("Performance Comparison:")
print("-" * 50)
for framework, metrics in benchmarks.items():
print(f"{framework:10}: {metrics['requests_sec']:5} req/s, {metrics['latency_ms']:.1f}ms latency")
Output:
Performance Comparison:
--------------------------------------------------
Flask : 1000 req/s, 5.0ms latency
Django : 800 req/s, 6.5ms latency
FastAPI : 3000 req/s, 1.5ms latency
Feature Comparison
| Feature | Flask | Django | FastAPI |
|---|---|---|---|
| Learning curve | Low | Medium | Medium |
| Async support | Experimental | Limited | Native |
| API docs | Manual | DRF | Auto |
| Validation | Manual | DRF | Pydantic |
| ORM | SQLAlchemy | Built-in | SQLAlchemy |
| Admin | Manual | Built-in | Manual |
| Auth | Extensions | Built-in | OAuth2 |
| Real-time | Manual | Channels | WebSocket |
| Scalability | High | High | Very High |
When to Use What?
# Flask:
# - Microservices
# - Small to medium APIs
# - Rapid prototyping
# - Learning web development
# Django:
# - Full-stack applications
# - Content management systems
# - E-commerce platforms
# - Enterprise applications
# FastAPI:
# - High-performance APIs
# - Microservices architecture
# - Real-time applications
# - ML model serving
ℹ️
Decision Framework: Choose based on project size, performance needs, and team expertise.
Real-World Patterns
Authentication
# Flask
from flask import Flask, request, jsonify
from functools import wraps
import jwt
app = Flask(__name__)
def token_required(f):
@wraps(f)
def decorated(*args, **kwargs):
token = request.headers.get('Authorization')
if not token:
return jsonify({'message': 'Token missing'}), 401
try:
data = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
current_user = data['user_id']
except:
return jsonify({'message': 'Invalid token'}), 401
return f(current_user, *args, **kwargs)
return decorated
# Django
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def protected_view(request):
return Response({'message': 'Hello, world!'})
# FastAPI
from fastapi import FastAPI, Depends, HTTPException
from fastapi.security import OAuth2PasswordBearer
app = FastAPI()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
async def get_current_user(token: str = Depends(oauth2_scheme)):
if not verify_token(token):
raise HTTPException(status_code=401)
return decode_token(token)
@app.get("/protected")
async def protected_route(current_user: dict = Depends(get_current_user)):
return {"message": f"Hello, {current_user['name']}"}
Database Patterns
# Flask + SQLAlchemy
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy(app)
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100))
# Django ORM
from django.db import models
class User(models.Model):
name = models.CharField(max_length=100)
email = models.EmailField(unique=True)
# FastAPI + SQLAlchemy
from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String(100))
Interview Tips
Common Follow-up Questions
-
"When would you choose Flask over Django?"
- Microservices architecture
- Need for minimal overhead
- Custom architecture requirements
- Small team with Python expertise
-
"How does FastAPI achieve better performance?"
- Starlette (async framework)
- Pydantic (C-based validation)
- No ORM overhead
- Native async/await
-
"What are the scalability considerations?"
- Flask: Horizontal scaling, load balancing
- Django: Database optimization, caching
- FastAPI: Async I/O, connection pooling
Code Review Tips
# BAD: No input validation (Flask)
@app.route('/api/users', methods=['POST'])
def create_user():
data = request.get_json()
user = User(**data) # No validation!
return jsonify(user)
# GOOD: With validation (FastAPI)
@app.post("/api/users")
async def create_user(user: UserCreate): # Pydantic validates
return {"status": "created"}
# BAD: Synchronous I/O in async (FastAPI)
@app.get("/api/users")
async def get_users():
users = db.query(User).all() # Blocks event loop!
return users
# GOOD: Async database (FastAPI)
@app.get("/api/users")
async def get_users():
async with db.session() as session:
result = await session.execute(select(User))
return result.scalars().all()
⚠️
Common Mistake: Using synchronous I/O in async frameworks blocks the event loop and reduces performance.
Summary
| Framework | Best For | Performance | Learning Curve |
|---|---|---|---|
| Flask | Microservices, prototyping | Good | Low |
| Django | Full-stack, enterprise | Good | Medium |
| FastAPI | High-performance APIs | Excellent | Medium |
Decision Matrix
| Requirement | Recommended |
|---|---|
| Rapid prototyping | Flask |
| Full-stack app | Django |
| High-performance API | FastAPI |
| Real-time features | FastAPI |
| Admin interface | Django |
| Microservices | FastAPI or Flask |
| ML model serving | FastAPI |
ℹ️
Key Takeaway: Choose the framework that fits your project needs. All three are excellent for different use cases.
Practice Problems
- Build API: Create a REST API with all three frameworks
- Compare Performance: Benchmark identical endpoints
- Authentication: Implement JWT auth in each framework
- Database Integration: Set up ORM with each framework
- Real-time: Add WebSocket support to FastAPI
Further Reading
- Flask Docs: https://flask.palletsprojects.com/
- Django Docs: https://docs.djangoproject.com/
- FastAPI Docs: https://fastapi.tiangolo.com/
- Benchmarks: TechEmpower Web Framework Benchmarks
Remember: The best framework is the one that fits your project requirements and team expertise.