Web Frameworks Proficient¶
When you'd use this
Flask, FastAPI and Django — building production web applications in Python.
Choose and use a web framework (FastAPI, Flask, Django) to build APIs and web apps with routing, request handling, and templating.
FastAPI (recommended for modern APIs)¶
Async, type-hint-driven framework with automatic validation and docs — the modern default for APIs.
Basic application¶
A full CRUD API in one file — Pydantic models validate input automatically, and the type hints generate interactive OpenAPI docs with zero extra work.
from fastapi import FastAPI, HTTPException, Depends, Query
from pydantic import BaseModel, Field
from typing import Optional
import uvicorn
app = FastAPI(title="My API", version="1.0.0")
# ─── Models ───────────────────────────────────────
class ItemCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
price: float = Field(..., gt=0)
description: Optional[str] = None
in_stock: bool = True
class ItemResponse(ItemCreate):
id: int
# ─── In-memory store ──────────────────────────────
items_db: dict[int, ItemResponse] = {}
next_id = 1
# ─── Routes ───────────────────────────────────────
@app.get("/")
async def root():
return {"message": "API is running", "items_count": len(items_db)}
@app.post("/items/", response_model=ItemResponse, status_code=201)
async def create_item(item: ItemCreate):
global next_id
new_item = ItemResponse(id=next_id, **item.model_dump())
items_db[next_id] = new_item
next_id += 1
return new_item
@app.get("/items/", response_model=list[ItemResponse])
async def list_items(
skip: int = Query(0, ge=0),
limit: int = Query(10, ge=1, le=100),
in_stock: Optional[bool] = None,
):
results = list(items_db.values())
if in_stock is not None:
results = [i for i in results if i.in_stock == in_stock]
return results[skip : skip + limit]
@app.get("/items/{item_id}", response_model=ItemResponse)
async def get_item(item_id: int):
if item_id not in items_db:
raise HTTPException(status_code=404, detail="Item not found")
return items_db[item_id]
@app.put("/items/{item_id}", response_model=ItemResponse)
async def update_item(item_id: int, item: ItemCreate):
if item_id not in items_db:
raise HTTPException(status_code=404, detail="Item not found")
updated = ItemResponse(id=item_id, **item.model_dump())
items_db[item_id] = updated
return updated
@app.delete("/items/{item_id}", status_code=204)
async def delete_item(item_id: int):
if item_id not in items_db:
raise HTTPException(status_code=404, detail="Item not found")
del items_db[item_id]
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
# Run:
uvicorn main:app --reload
# Auto-generated docs available at:
# http://127.0.0.1:8000/docs (Swagger UI)
# http://127.0.0.1:8000/redoc (ReDoc)
Dependency Injection¶
Declare what a route needs (a DB session, the current user) as a parameter, and FastAPI supplies it — centralizing shared setup and making routes trivial to test with fakes.
FastAPI's Depends lets a route ask for the things it needs instead of building them itself. You write a small provider function once, then any route that needs it adds a parameter — FastAPI calls the provider, passes in the result, and (for generator dependencies like get_db) runs the cleanup afterwards.
from fastapi import Depends, Header, HTTPException
# Database session dependency
async def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
# Auth dependency
async def get_current_user(authorization: str = Header(...)):
token = authorization.replace("Bearer ", "")
user = decode_token(token)
if not user:
raise HTTPException(status_code=401, detail="Invalid token")
return user
# Use in routes
@app.get("/profile")
async def profile(user = Depends(get_current_user), db = Depends(get_db)):
return db.query(User).filter_by(id=user.id).first()
The payoff is testability: in a test you override get_current_user and get_db with fakes (app.dependency_overrides[get_db] = fake_db), so the route runs with no real database or token — the same decoupling you'd get from passing collaborators into a constructor, but wired by the framework.
Middleware¶
Code that runs on every request/response — use it for cross-cutting concerns like timing, logging, CORS, or auth that shouldn't be duplicated in each route.
import time
from fastapi import Request
@app.middleware("http")
async def timing_middleware(request: Request, call_next):
start = time.perf_counter()
response = await call_next(request)
elapsed = time.perf_counter() - start
response.headers["X-Process-Time"] = f"{elapsed:.4f}"
return response
Background Tasks¶
Return a response immediately and run slow work (sending email, writing logs) after — so the client isn't left waiting on a task whose result it doesn't need.
from fastapi import BackgroundTasks
def send_email(email: str, message: str):
# Slow operation — runs in background
import time
time.sleep(2)
print(f"Email sent to {email}")
@app.post("/notify/")
async def notify(email: str, background_tasks: BackgroundTasks):
background_tasks.add_task(send_email, email, "Welcome!")
return {"message": "Notification queued"}
Flask (lightweight, flexible)¶
A minimal framework you extend as needed — use for small apps and when you want control.
Basic application¶
The same CRUD API in Flask — explicit routes and manual JSON handling, with errorhandler for consistent error responses. Compare the hand-written validation here to FastAPI's automatic version above.
from flask import Flask, request, jsonify, abort
app = Flask(__name__)
items = []
@app.route("/")
def index():
return jsonify({"status": "ok", "items": len(items)})
@app.route("/items", methods=["GET"])
def get_items():
page = request.args.get("page", 1, type=int)
per_page = request.args.get("per_page", 10, type=int)
start = (page - 1) * per_page
return jsonify(items[start:start + per_page])
@app.route("/items", methods=["POST"])
def create_item():
data = request.get_json()
if not data or "name" not in data:
abort(400, description="Name is required")
item = {"id": len(items) + 1, **data}
items.append(item)
return jsonify(item), 201
@app.route("/items/<int:item_id>", methods=["GET"])
def get_item(item_id):
item = next((i for i in items if i["id"] == item_id), None)
if not item:
abort(404, description="Item not found")
return jsonify(item)
@app.errorhandler(404)
def not_found(error):
return jsonify({"error": str(error.description)}), 404
@app.errorhandler(400)
def bad_request(error):
return jsonify({"error": str(error.description)}), 400
if __name__ == "__main__":
app.run(debug=True)
Flask Blueprints (modular structure)¶
Split a growing app into self-contained modules, each with its own routes and URL prefix, then register them on the main app — the standard way to keep large Flask projects organized.
# users/routes.py
from flask import Blueprint, jsonify
users_bp = Blueprint("users", __name__, url_prefix="/users")
@users_bp.route("/")
def list_users():
return jsonify([])
@users_bp.route("/<int:user_id>")
def get_user(user_id):
return jsonify({"id": user_id})
# app.py
from flask import Flask
from users.routes import users_bp
app = Flask(__name__)
app.register_blueprint(users_bp)
Flask with SQLAlchemy¶
Add a database to Flask by defining models as Python classes — Flask-SQLAlchemy maps them to tables and gives you an ORM, since Flask (unlike Django) ships without one.
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///app.db"
db = SQLAlchemy(app)
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(200), unique=True)
with app.app_context():
db.create_all()
Django (full-featured framework)¶
Batteries-included framework with ORM, admin, and auth — use for content-heavy, database-backed sites.
Project structure¶
How a Django project is laid out — a top-level project package for settings/URLs, and one or more "apps" each holding their own models, views, and tests. Knowing this layout is key to navigating any Django codebase.
myproject/
├── manage.py
├── myproject/
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
└── myapp/
├── models.py
├── views.py
├── serializers.py
├── urls.py
└── tests.py
Django REST Framework — API views¶
Build a full REST API from a model with almost no boilerplate — a ModelViewSet plus a router gives you list/create/retrieve/update/delete endpoints, filtering, and search for free.
# models.py
from django.db import models
class Article(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
published = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ["-created_at"]
# serializers.py
from rest_framework import serializers
class ArticleSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = "__all__"
# views.py
from rest_framework import viewsets
from .models import Article
from .serializers import ArticleSerializer
class ArticleViewSet(viewsets.ModelViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
filterset_fields = ["published"]
search_fields = ["title", "content"]
# urls.py
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register("articles", ArticleViewSet)
urlpatterns = router.urls
Framework comparison¶
How FastAPI, Flask, and Django differ so you can pick for your project's needs.
| Feature | FastAPI | Flask | Django |
|---|---|---|---|
| Speed | Very fast (async, Starlette) | Good | Good |
| Type safety | Built-in (Pydantic) | Manual | Manual (DRF serializers) |
| Auto-docs | Yes (OpenAPI/Swagger) | No (use flasgger) | Yes (DRF browsable API) |
| ORM | No (bring your own) | No (use Flask-SQLAlchemy) | Yes (built-in) |
| Admin panel | No | No | Yes (built-in) |
| Auth | Manual (fastapi-users) | Manual (Flask-Login) | Built-in |
| Async | Native | Extension (quart) | Partial (Django 4.1+) |
| Learning curve | Medium | Low | High |
| Best for | APIs, microservices | Simple apps, prototypes | Full web apps, CMS |
| When to choose | Modern REST/GraphQL APIs | Quick scripts, small APIs | Large apps with admin |
Deployment patterns¶
Run a web app in production behind an ASGI/WSGI server and process manager.
# FastAPI with Gunicorn + Uvicorn workers
# gunicorn main:app -w 4 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000
# Flask with Gunicorn
# gunicorn app:app -w 4 --bind 0.0.0.0:5000
# Django with Gunicorn
# gunicorn myproject.wsgi:application -w 4 --bind 0.0.0.0:8000
Docker deployment¶
Package the app and its dependencies into a reproducible image so it runs identically on any host — the standard unit of deployment for modern web services.
FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Testing web applications¶
Exercise routes with a test client so you can assert on responses without a live server.
# FastAPI testing
from fastapi.testclient import TestClient
client = TestClient(app)
def test_create_item():
response = client.post("/items/", json={"name": "Widget", "price": 9.99})
assert response.status_code == 201
data = response.json()
assert data["name"] == "Widget"
assert "id" in data
def test_get_nonexistent():
response = client.get("/items/9999")
assert response.status_code == 404
# Flask testing
def test_flask_app():
with app.test_client() as client:
response = client.get("/")
assert response.status_code == 200
Practice Exercises¶
- Build a complete CRUD API with FastAPI — User model with registration, login (JWT), profile CRUD.
- Build the same API in Flask and compare the code volume and structure.
- Add pagination, filtering, and sorting to a list endpoint.
- Implement rate limiting middleware that blocks after N requests per minute.
- Write integration tests for all endpoints using TestClient.
- Dockerize the application and deploy with docker-compose (app + PostgreSQL + Redis).
💬 Discussion
Have a question about this topic? Found an error? Share your thoughts below.