Skip to content

Web Frameworks Proficient

🌐 Web & APIs Track · Level 4
⏱️ ~2 weeks 📚 Prerequisites: Decorators, Type Hints

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.

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()

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

  1. Build a complete CRUD API with FastAPI — User model with registration, login (JWT), profile CRUD.
  2. Build the same API in Flask and compare the code volume and structure.
  3. Add pagination, filtering, and sorting to a list endpoint.
  4. Implement rate limiting middleware that blocks after N requests per minute.
  5. Write integration tests for all endpoints using TestClient.
  6. 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.