1 FastAPI Assignments
teacher edited this page 2026-04-13 13:47:07 +02:00

FastAPI Assignments — Complete Tutorial Guide

This guide walks through all ten assignments in the fastapi-assignments/ directory. Each section explains not only how to run the code, but why things are structured the way they are. The target reader is someone who has basic Python knowledge but has never built a web API before.


Prerequisites

Before starting any assignment, ensure the following are in place.

Python Version

FastAPI requires Python 3.8 or later. Verify your installation:

python --version

Virtual Environments

A virtual environment is an isolated Python installation that keeps the dependencies of one project separate from another. Every assignment should be run inside its own virtual environment.

To create and activate one:

# Create the environment (run once per assignment folder)
python -m venv venv

# Activate on macOS/Linux
source venv/bin/activate

# Activate on Windows
venv\Scripts\activate

Your shell prompt will change to show (venv) when the environment is active. Always activate before installing dependencies or running the server.

Tools for Testing

FastAPI generates interactive documentation automatically. For most assignments you can test directly in the browser at http://127.0.0.1:8000/docs. For more control, install Postman or use curl from the terminal.


Assignment 01: Basic To-Do API

Location: fastapi-assignments/01-basic-todo-api/

What You Will Learn

  • How to create a FastAPI application and define routes
  • The four fundamental HTTP methods: GET, POST, PUT, DELETE
  • How to validate incoming data with Pydantic models
  • How path parameters work
  • The concept of in-memory storage and its limitations

Key Concepts

HTTP methods correspond to operations on a resource. By convention: POST creates, GET reads, PUT replaces/updates, and DELETE removes. FastAPI uses decorators (@app.get, @app.post, etc.) to bind a function to a method and a URL path.

Pydantic is a data validation library that uses Python type hints. When you define a class that inherits from BaseModel, FastAPI uses it to validate the request body automatically. If the client sends incorrect data (for example, a number where a string is expected), FastAPI returns a 422 Unprocessable Entity response before your code even runs.

Path parameters are variable segments of a URL. In the path /todos/{todo_id}, the segment {todo_id} is captured and passed to your function. FastAPI also converts it to the annotated type: todo_id: int means the string "5" from the URL becomes the integer 5 in Python.

Setup

pip install fastapi uvicorn[standard]

Uvicorn is an ASGI web server. ASGI (Asynchronous Server Gateway Interface) is the protocol that connects FastAPI to the network. You always need an ASGI server to run a FastAPI application.

Running the Server

uvicorn main:app --reload

This tells Uvicorn to import the app object from main.py. The --reload flag causes the server to restart automatically whenever you save a file, which is useful during development.

Project Files

schemas.py

This file defines three Pydantic models:

  • TodoCreate — the shape of data the client sends when creating a task. It contains title (a non-empty string) and an optional description.
  • TodoUpdate — the shape of data the client sends when editing a task. All fields are optional because a partial update should be possible (only updating the title, for example).
  • TodoResponse — the shape of data the server returns. It extends the base fields with id (assigned by the server) and completed (a boolean).

The separation into three models is intentional. The client should never be able to set the id or the completed flag during creation — those are server-controlled fields. Separating input and output models enforces this boundary.

main.py

The application stores todos in a plain Python list (todos: list = []) and a counter for IDs (next_id: int = 1). This data lives only in RAM. When the process stops, everything is lost.

Each route function:

  1. Receives the validated request data from FastAPI
  2. Operates on the in-memory list
  3. Returns either data (which FastAPI serialises to JSON) or raises an HTTPException

HTTPException is FastAPI's way of returning error responses. Calling raise HTTPException(status_code=404, detail="Not found") immediately terminates the handler and sends a 404 response to the client.

Testing Step by Step

  1. Start the server: uvicorn main:app --reload
  2. Open http://127.0.0.1:8000/docs
  3. Click POST /todos, then "Try it out". Enter a body like {"title": "Buy groceries"} and click "Execute". Note the id in the response.
  4. Click GET /todos and execute. You should see your item in the list.
  5. Click PUT /todos/{todo_id}. Enter the id from step 3 and a body like {"title": "Buy groceries and milk"}.
  6. Click DELETE /todos/{todo_id} with the same id.
  7. Execute GET /todos again. The list should be empty.

Common Issues

  • ModuleNotFoundError: No module named 'fastapi' — the virtual environment is not activated, or dependencies were not installed.
  • Port already in use — another process is using port 8000. Stop it, or run with uvicorn main:app --reload --port 8001.

Assignment 02: Personal Blog

Location: fastapi-assignments/02-personal-blog/

What You Will Learn

  • How to integrate a relational database using SQLAlchemy
  • The concept of an ORM (Object-Relational Mapper)
  • Dependency injection in FastAPI
  • URL slugs: what they are and why they matter
  • How data persists across server restarts

Key Concepts

SQLAlchemy is a Python SQL toolkit and ORM. The ORM layer allows you to interact with a database using Python classes and objects instead of writing raw SQL. You define a class (Post), and SQLAlchemy translates your Python operations into SQL statements.

SQLite is a file-based relational database. Unlike server-based databases (PostgreSQL, MySQL), SQLite stores everything in a single file (blog.db). It requires no installation or configuration, making it ideal for learning.

Dependency injection is a design pattern where a function declares what it needs (its dependencies), and a framework provides them at call time. In FastAPI, dependencies are declared using Depends. The get_db function creates a database session, yields it to the route handler, and then closes it — even if the handler raises an exception.

A slug is a URL-friendly identifier derived from human-readable text. For a post titled "My First Post", the slug would be my-first-post. Slugs make URLs meaningful and stable. They are typically lowercase, hyphen-separated, and contain no special characters.

Setup

pip install fastapi uvicorn[standard] sqlalchemy

Running the Server

uvicorn main:app --reload

On first run, SQLAlchemy creates the blog.db file and the posts table automatically.

Project Files

database.py

This file contains three components:

  1. engine — the connection to the SQLite file. The string sqlite:///./blog.db means "SQLite, relative path, file named blog.db".
  2. SessionLocal — a factory that creates database sessions. Each HTTP request gets its own session.
  3. get_db — a generator function used as a FastAPI dependency. The try/finally block ensures the session is closed even if an error occurs during the request.

models.py

Defines the Post class that maps to the posts table. The Base.metadata.create_all(bind=engine) call in main.py reads all classes that inherit from Base and creates the corresponding tables if they don't exist.

schemas.py

Contains PostCreate (input) and PostResponse (output) Pydantic models. Note PostResponse has model_config = ConfigDict(from_attributes=True). This tells Pydantic that it can read data from SQLAlchemy model attributes (not just plain dictionaries), enabling seamless conversion between the ORM object and the JSON response.

The slug field in PostCreate uses a regex constraint: pattern=r'^[a-z0-9]+(?:-[a-z0-9]+)*$'. This ensures only valid slugs are accepted — the regex requires lowercase letters or digits, with hyphens only between segments.

main.py

Route handlers receive db: Session = Depends(get_db). FastAPI calls get_db, gets the session object, and passes it to the handler. After the handler returns, FastAPI resumes get_db to close the session.

The GET /posts/{slug} endpoint uses db.query(Post).filter(Post.slug == slug).first(). If no post matches, .first() returns None, and the handler raises a 404 exception.

Testing Step by Step

  1. Start the server.
  2. Create a post: POST /posts with body {"title": "Hello World", "slug": "hello-world", "content": "My first post."}.
  3. Retrieve it: GET /posts/hello-world.
  4. Stop and restart the server. Then retrieve the post again. It is still there — unlike Assignment 01, the data persisted.
  5. Try creating a second post with the same slug. The server should return a 409 Conflict error because slugs must be unique.

Assignment 03: User Profile & Uploads

Location: fastapi-assignments/03-user-profile-uploads/

What You Will Learn

  • How to accept file uploads via multipart form data
  • How to serve static files (images, CSS) over HTTP
  • The difference between JSON bodies and form data
  • Basic file path security

Key Concepts

Multipart form data is an encoding format for HTTP requests that can carry both text fields and binary files simultaneously. Standard JSON cannot transport binary data. When a browser submits a form with a file input, it uses multipart encoding automatically.

Static file serving means making files on the server's filesystem accessible via HTTP URLs. FastAPI's StaticFiles middleware maps a URL prefix to a local directory. A file saved at static/uploads/avatar.png becomes reachable at http://localhost:8000/static/uploads/avatar.png.

secure_filename (from Werkzeug, used indirectly) sanitises user-supplied filenames to prevent path traversal attacks. An attacker could attempt to upload a file named ../../etc/passwd to overwrite system files. secure_filename strips path separators and other dangerous characters.

Setup

pip install fastapi uvicorn[standard] python-multipart

The python-multipart package is required for FastAPI to parse form data and file uploads. Without it, FastAPI cannot process multipart/form-data requests.

Running the Server

uvicorn main:app --reload

The server creates a static/uploads/ directory if it does not exist.

Project Files

main.py

The line app.mount("/static", StaticFiles(directory="static"), name="static") attaches the static file middleware. Any file inside the static/ folder becomes directly downloadable at the corresponding URL path.

The upload_profile_image endpoint uses UploadFile = File(...). UploadFile is a FastAPI wrapper around the uploaded file that provides:

  • .filename — the original name the client gave the file
  • .content_type — the MIME type (e.g., image/jpeg)
  • .read() — read the file contents as bytes (async)

The file is written to disk using shutil.copyfileobj(file.file, buffer), which streams the file from the upload buffer directly to the output file without loading everything into memory at once.

The update_profile endpoint uses bio: str = Form(None). Form(None) tells FastAPI to read this parameter from the form body rather than from a JSON body. The None default makes the field optional.

schemas.py

ProfileResponse has profile_image_url: Optional[str] = None. When a profile has no image, this field is null in the JSON response.

Testing Step by Step

  1. Start the server.
  2. GET /profiles/john_doe — note that profile_image_url is null.
  3. In the Swagger UI, use POST /profiles/john_doe/upload-image. Click "Try it out", then click the file selector and upload any image.
  4. The response contains a profile_image_url like /static/uploads/john_doe.jpg.
  5. Open http://127.0.0.1:8000/static/uploads/john_doe.jpg in your browser — the image is served directly.
  6. PATCH /profiles/john_doe with form field bio=Web developer. The profile bio is updated.

Common Issues

  • 422 Unprocessable Entity on file upload — the python-multipart package is not installed.
  • FileNotFoundError — the static/uploads/ directory does not exist. Create it manually: mkdir -p static/uploads.

Assignment 04: Task Manager with Background Tasks

Location: fastapi-assignments/04-task-manager-bg-tasks/

What You Will Learn

  • What background tasks are and why they matter
  • FastAPI's BackgroundTasks mechanism
  • Why web servers must respond quickly
  • How to simulate slow I/O operations for testing

Key Concepts

The request-response cycle has a fundamental constraint: the client waits for a response. If a route handler takes 30 seconds to run (sending an email, generating a report), the client waits 30 seconds. This is unacceptable for user-facing applications.

Background tasks break this constraint. FastAPI's BackgroundTasks object lets you schedule a function to run after the response has been sent. The client receives a response immediately; the slow work happens in the background.

This pattern is suitable for non-critical work where immediate confirmation is not required — sending welcome emails, writing to audit logs, triggering report generation. For critical operations (processing a payment, for example), you need a proper task queue (Celery, RQ, etc.) with persistence and retry logic.

EmailStr is a Pydantic type that validates email addresses using a standard algorithm. It rejects obviously invalid strings like "not-an-email" before the request reaches your handler.

Setup

pip install fastapi uvicorn[standard] "pydantic[email]"

The pydantic[email] extra installs the email-validator library that powers EmailStr.

Running the Server

uvicorn main:app --reload

Project Files

schemas.py

TaskCreate requires title: str and user_email: EmailStr. The EmailStr type is imported from pydantic. Pydantic validates that the email is syntactically valid before the handler runs.

email_utils.py

The send_task_notification function uses time.sleep(5) to simulate the latency of connecting to an external email service. Real email delivery involves network round-trips that can take seconds. The logging module writes messages to the terminal so you can observe when the background task starts and finishes.

main.py

The complete_task endpoint has this signature:

async def complete_task(task_id: int, background_tasks: BackgroundTasks):

FastAPI injects BackgroundTasks automatically — you do not need Depends. Calling background_tasks.add_task(send_task_notification, ...) registers the function but does not call it yet. FastAPI calls it after the response is delivered.

Testing Step by Step

  1. Start the server with your terminal visible.
  2. Create a task: POST /tasks with body {"title": "Write report", "user_email": "user@example.com"}. Note the returned id (e.g., 1).
  3. Mark it complete: POST /tasks/1/complete.
  4. Observe the terminal carefully. The Swagger UI shows the response immediately. In the terminal, you will see Starting background task..., then — after five seconds — Notification SENT to user@example.com.

This delay demonstrates the asynchronous nature of background tasks: the HTTP response was sent before the simulated email work finished.


Assignment 05: Weather Dashboard

Location: fastapi-assignments/05-weather-dashboard/

What You Will Learn

  • How to make HTTP requests from within a FastAPI application
  • Asynchronous HTTP clients (httpx)
  • In-memory caching with TTL (time-to-live)
  • Query parameter validation

Key Concepts

External API integration means your application acts as a client to a third-party service. Your server receives a request, forwards a request to an external API, processes the response, and returns the result to the original client. The chain is: browser → your FastAPI app → Open-Meteo API.

httpx is an asynchronous HTTP client for Python, similar to the popular requests library but with native async/await support. When your handler calls await client.get(url), Python can process other requests while waiting for the external response — the server is never blocked.

Caching stores the result of an expensive computation so future requests can reuse it without repeating the computation. Here, the "expensive" part is the outbound HTTP call to Open-Meteo. Cached responses are served immediately; real API calls are made only when the cache is empty or the data is too old.

TTL (time-to-live) defines how long a cached entry is valid. Weather data changes every hour; a 15-minute TTL means the cache is refreshed at most four times per hour per coordinate pair. After the TTL expires, the next request triggers a fresh API call.

Setup

pip install fastapi uvicorn[standard] httpx

Running the Server

uvicorn main:app --reload

This application requires an internet connection because it calls the Open-Meteo API.

Project Files

weather_service.py

WeatherService.get_weather uses async with httpx.AsyncClient() as client to create a managed HTTP client. The async with statement ensures the connection is properly closed when the block exits, even if an exception occurs.

The Open-Meteo API endpoint https://api.open-meteo.com/v1/forecast accepts latitude, longitude, and a list of variables. The response JSON structure is:

{
  "current_weather": {
    "temperature": 15.2,
    ...
  }
}

schemas.py

WeatherRequest validates the query parameters:

  • latitude: float = Query(ge=-90, le=90) — must be between -90 and 90
  • longitude: float = Query(ge=-180, le=180) — must be between -180 and 180

The city_name field is optional (Optional[str] = None). The API does not look up cities by name itself — it needs coordinates. The city name is accepted purely for informational purposes and is included in the response.

WeatherResponse includes a cached: bool field. When the response comes from cache, this is True; when it comes from the real API, it is False. This flag helps you verify that caching works.

main.py

The cache is a dictionary: weather_cache: dict = {}. The key is a tuple of rounded coordinates (round(lat, 1), round(lon, 1)). Rounding prevents cache misses caused by trivial differences like 51.5001 vs 51.5002.

Each cache entry is a dict with data (the weather response) and timestamp (when it was fetched). The TTL check is:

if time.time() - entry["timestamp"] < 900:  # 900 seconds = 15 minutes
    return cached data

Testing Step by Step

  1. Start the server.
  2. GET /weather?city_name=London&latitude=51.5&longitude=-0.12
  3. Note "cached": false in the response.
  4. Repeat the exact same request immediately.
  5. Note "cached": true — the response came from cache and was near-instantaneous.
  6. Wait 15 minutes (or temporarily reduce the TTL to 10 seconds for testing) and repeat. "cached": false again.

Assignment 06: Real-time Chat

Location: fastapi-assignments/06-real-time-chat/

What You Will Learn

  • The WebSocket protocol and how it differs from HTTP
  • Connection management across multiple clients
  • Broadcasting messages to all connected clients
  • Handling disconnections gracefully

Key Concepts

HTTP vs WebSockets: HTTP is a request-response protocol — the client sends a request, the server sends a response, and the connection is closed (or kept alive for reuse but remains idle). WebSockets establish a persistent, bidirectional channel. Once the connection is open, both parties can send messages at any time without a new request.

The WebSocket handshake starts as an HTTP request with special headers (Upgrade: websocket). The server responds with 101 Switching Protocols, and from that point forward the TCP connection carries WebSocket frames instead of HTTP.

Broadcasting is the act of sending a message to multiple recipients. When a client sends a chat message, the server must forward it to every other connected client. This requires the server to maintain a list of all active connections.

Client disconnection can happen in several ways: the user closes the tab, the network drops, or the browser is closed. FastAPI raises a WebSocketDisconnect exception when this happens. Catching this exception allows the server to remove the disconnected client from its list.

Setup

pip install fastapi uvicorn[standard]

No additional packages are required — FastAPI includes WebSocket support natively.

Running the Server

uvicorn main:app --reload

Project Files

chat_manager.py

The ConnectionManager class maintains a dictionary active_connections: dict[int, WebSocket]. The key is a client ID (a random integer assigned when connecting), and the value is the WebSocket object.

Four methods:

  • connect(client_id, websocket) — calls await websocket.accept() to complete the WebSocket handshake, then adds the connection to the dictionary.
  • disconnect(client_id) — removes the connection from the dictionary.
  • send_personal_message(message, client_id) — sends a message to one specific client.
  • broadcast(message, sender_id) — iterates over all connections and sends the message to everyone except the sender.

main.py

The root route (GET /) returns an HTMLResponse containing a complete HTML page with embedded JavaScript. The page creates a WebSocket object pointing to ws://localhost:8000/ws/{clientId} and registers event handlers: onmessage (display incoming messages), onclose (show disconnection notice).

The WebSocket endpoint:

@app.websocket("/ws/{client_id}")
async def websocket_endpoint(websocket: WebSocket, client_id: int):
    await manager.connect(client_id, websocket)
    try:
        while True:
            data = await websocket.receive_text()
            await manager.broadcast(f"Client {client_id}: {data}", client_id)
    except WebSocketDisconnect:
        manager.disconnect(client_id)
        await manager.broadcast(f"Client {client_id} left the chat", client_id)

The while True loop keeps the connection alive, waiting for messages. receive_text() blocks (asynchronously) until a message arrives.

Testing Step by Step

  1. Start the server.
  2. Open http://127.0.0.1:8000/ in one browser tab.
  3. Open the same URL in a second tab.
  4. Each tab shows a different Client ID.
  5. Type a message in Tab 1 and click "Send". The message appears in Tab 2 (and Tab 1).
  6. Close Tab 1. Tab 2 shows "Client X left the chat".

Assignment 07: Form Handling & Jinja2 Templates

Location: fastapi-assignments/07-forms-jinja2/

What You Will Learn

  • Server-side HTML rendering with Jinja2
  • Processing HTML form submissions
  • Server-side validation with user-facing error messages
  • The GET/POST pattern for forms
  • Pre-filling form fields after validation failure

Key Concepts

Server-side rendering (SSR) means the server generates complete HTML pages and sends them to the browser. The browser displays the HTML without running any JavaScript application framework. This is the traditional approach used by Django, Rails, and classic PHP applications.

Jinja2 is a template engine. A template is an HTML file with special placeholders ({{ variable }}) and control structures ({% if condition %}, {% for item in list %}). The server fills in the placeholders with data and sends the resulting HTML to the client.

The POST/Redirect/GET pattern: After a form submission that modifies state (creating a record), you should redirect to a GET endpoint rather than rendering a response directly. This prevents the browser from re-submitting the form if the user presses the Back button or refreshes the page. This assignment uses a simplified version — it renders a success page directly — but a real application would use RedirectResponse.

Setup

pip install fastapi uvicorn[standard] jinja2 python-multipart

Jinja2 is FastAPI's default template engine. python-multipart is required to parse form submissions.

Running the Server

uvicorn main:app --reload

Project Files

main.py

Jinja2Templates(directory="templates") creates a template renderer pointing at the templates/ directory. The directory path is relative to where you run uvicorn.

The get_subscription_form route returns a TemplateResponse. The context dictionary must include {"request": request} — this is a Jinja2/FastAPI requirement. Additional variables (like error messages or previously submitted values) are also passed in the context.

The handle_subscription route:

  1. Reads form fields: name: str = Form(...) and email: str = Form(...).
  2. Validates them manually (length check for name, regex for email).
  3. If validation fails: re-renders index.html with the original values and error messages, so the user does not have to re-type everything.
  4. If validation passes: appends to the database and renders success.html.

templates/index.html

Jinja2 conditional syntax: {% if error_name %}<p class="error">{{ error_name }}</p>{% endif %}.

Pre-filling uses: value="{{ name if name else '' }}". If name was passed in the context, it fills the input field; otherwise the field is empty.

templates/success.html

A simple page that renders {{ name }} and {{ email }} from the context.

Testing Step by Step

  1. Start the server. Open http://127.0.0.1:8000/.
  2. Submit the form with a valid name and email. You should see a success page.
  3. Go back. Submit with a name of one character (e.g., "A"). The form re-renders with an error message, and the email field is pre-filled with what you entered.
  4. Submit with an invalid email (e.g., "not-an-email"). The form re-renders with the email error, and the name field retains its value.

Assignment 08: Webhook Receiver

Location: fastapi-assignments/08-webhook-receiver/

What You Will Learn

  • What webhooks are and how they are used in practice
  • HMAC signature verification
  • Why raw request bodies must be used for signature verification
  • Constant-time comparison and timing attack prevention

Key Concepts

Webhooks are HTTP callbacks. Instead of your application polling an external service for updates, the external service pushes notifications to your application by sending HTTP POST requests. GitHub sends webhook events when code is pushed; Stripe sends them when a payment succeeds. Your application must expose a publicly accessible URL to receive them.

HMAC (Hash-based Message Authentication Code) is a mechanism for verifying both the integrity and authenticity of a message. The sender and receiver share a secret key. The sender computes HMAC(key, message) and attaches it to the request. The receiver independently computes HMAC(key, received_message) and compares it to the provided signature. If they match, the message was not tampered with and came from someone who knows the key.

Raw body requirement: JSON parsers do not preserve insignificant whitespace or key ordering. Two JSON representations of the same data may produce different byte sequences. HMAC is computed over bytes, not over parsed objects. Therefore, the signature must be verified against the exact byte sequence received, not against a re-serialised Python dict.

Timing attacks: A naive string comparison (a == b) returns False as soon as it finds the first differing character. This means comparisons over invalid signatures finish slightly faster than comparisons over almost-valid ones. An attacker can measure these timing differences to deduce the correct signature character by character. hmac.compare_digest takes the same time regardless of where the strings diverge, eliminating this vector.

Setup

pip install fastapi uvicorn[standard]

All required cryptographic functions (hmac, hashlib) are part of the Python standard library.

Running the Server

uvicorn main:app --reload

Project Files

webhook_utils.py

def verify_signature(payload: bytes, secret: bytes, signature: str) -> bool:
    expected = hmac.new(secret, payload, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)

hmac.new creates an HMAC object using sha256 as the digest algorithm. .hexdigest() returns the result as a lowercase hexadecimal string.

main.py

body = await request.body()

This reads the raw bytes before any JSON parsing. The signature check runs against body. Only after a successful verification is the body parsed:

payload = json.loads(body)

The endpoint returns a 403 Forbidden (in some implementations, 401 Unauthorized) if the signature is missing or invalid, logging nothing about the specific payload to avoid leaking information.

Testing Step by Step

Use the Python script from the README to send a correctly signed request. Then modify the secret in the script to something wrong and observe the 401 response. This confirms that unsigned or incorrectly signed requests are rejected.


Assignment 09: Inventory Management

Location: fastapi-assignments/09-inventory-management/

What You Will Learn

  • API versioning strategies and their trade-offs
  • Pagination with offset and limit
  • Structured vs unstructured pagination responses
  • Organising a FastAPI application with APIRouter

Key Concepts

API versioning allows a server to evolve its API without breaking existing clients. The URL prefix approach (/v1, /v2) makes the version explicit and visible. When V2 introduces breaking changes (different response shape, removed fields), V1 remains available for clients that have not migrated yet.

Pagination is necessary when a resource collection is too large to return in a single response. Instead of returning all items, the API accepts offset (how many items to skip) and limit (how many items to return). A client retrieves the first page with offset=0&limit=20, the second page with offset=20&limit=20, and so on.

has_more is a flag in the V2 response that tells the client whether more pages exist. It is computed as offset + limit < total. Without this flag, the client must make an extra request to discover it has reached the last page.

APIRouter organises related routes into groups. Each router can have its own prefix, tags, and middleware. The main application then includes routers: app.include_router(v1_router). This is analogous to Flask's Blueprints.

Setup

pip install fastapi uvicorn[standard]

Running the Server

uvicorn main:app --reload

Project Files

schemas.py

PaginatedItems is the V2 response model:

class PaginatedItems(BaseModel):
    items: list[ItemResponse]
    total: int
    offset: int
    limit: int
    has_more: bool

V1 returns list[ItemResponse] directly — a simpler structure without metadata.

v1/router.py

router = APIRouter(prefix="/v1", tags=["Inventory V1"])

The endpoint returns a slice: inventory_data[offset : offset + limit]. No metadata — just the raw list.

v2/router.py

router = APIRouter(prefix="/v2", tags=["Inventory V2"])

The endpoint filters by category (if provided), computes total, calculates has_more, and returns a PaginatedItems object.

main.py

app.include_router(v1_router)
app.include_router(v2_router)

Both routers are registered on the same application. The documentation at /docs shows all routes, grouped by tags.

Testing Step by Step

  1. GET /v1/items?offset=0&limit=5 — returns 5 items as a JSON array.
  2. GET /v1/items?offset=5&limit=5 — returns the next 5.
  3. GET /v2/items?offset=0&limit=5 — returns a structured object with items, total, has_more.
  4. GET /v2/items?category=Electronics&offset=0&limit=10 — filtered results.
  5. Note the difference in response structure between V1 and V2.

Assignment 10: Secure Document Vault

Location: fastapi-assignments/10-secure-document-vault/

What You Will Learn

  • JWT (JSON Web Token) authentication
  • Password hashing with bcrypt
  • OAuth2 flows in FastAPI
  • Scope-based authorisation
  • Role-Based Access Control (RBAC) via clearance levels

Key Concepts

Authentication answers "who are you?" — verifying the identity of a user. Authorisation answers "what are you allowed to do?" — verifying that the identified user has permission for a specific action. These are distinct concepts that must both be implemented correctly.

Passwords must never be stored in plain text. A password hash is a one-way transformation: given password, you compute hash(password). Verification works by hashing the input and comparing with the stored hash. Even if the database is compromised, attackers cannot recover the original passwords from hashes. Bcrypt is a deliberately slow hashing algorithm designed for passwords. Its slowness makes brute-force attacks impractical.

JWT (JSON Web Token) is a compact, URL-safe token format. It consists of three Base64-encoded parts separated by dots: a header (algorithm), a payload (claims), and a signature. The server issues a JWT when the user authenticates. Subsequent requests include the JWT in the Authorization: Bearer <token> header. The server verifies the signature without consulting a database, making JWTs stateless.

OAuth2 Scopes are granular permissions embedded in the JWT. A token might carry scopes ["vault:read", "vault:write"]. An endpoint requiring vault:write will reject tokens that only carry vault:read. This allows issuing tokens with the minimum required permissions.

Clearance levels implement object-level access control. A user with clearance level 3 cannot read a document classified at level 4, even if they have the vault:read scope. This is enforced in the route handler by comparing current_user["clearance_level"] with document["secret_level"].

Setup

pip install fastapi uvicorn[standard] python-jose[cryptography] passlib[bcrypt] python-multipart
  • python-jose — JWT encoding and decoding
  • passlib[bcrypt] — password hashing
  • python-multipart — required for OAuth2 form submissions

Running the Server

uvicorn main:app --reload

Project Files

auth.py

OAuth2PasswordBearer with scopes tells FastAPI's documentation UI to render an authorisation dialog where users can select which scopes to request.

pwd_context = CryptContext(schemes=["bcrypt"]) creates a Passlib context. pwd_context.verify(plain, hashed) checks a password; pwd_context.hash(password) creates a new hash.

create_access_token builds the JWT payload and signs it using jwt.encode(data, SECRET_KEY, algorithm=ALGORITHM). The payload includes the sub (subject, i.e., username) and scopes.

get_current_user is a FastAPI dependency that:

  1. Receives the bearer token from the request header automatically (via OAuth2PasswordBearer)
  2. Decodes the JWT using jwt.decode
  3. Looks up the user in the mock database
  4. Checks that the token carries all scopes required by the calling endpoint (passed via security_scopes.scopes)

main.py

The /token endpoint uses OAuth2PasswordRequestForm — a FastAPI built-in that reads username and password from a form body. After authentication, it issues a JWT with the user's scopes.

Protected endpoints declare their requirements:

@app.get("/documents")
async def get_documents(current_user=Security(get_current_user, scopes=["vault:read"])):

Security is like Depends but carries scope information. FastAPI passes the required scopes to get_current_user via security_scopes.

Testing Step by Step

  1. Open http://127.0.0.1:8000/docs.
  2. Click "Authorize" (top right). Enter username alice, password alice123, and select scopes vault:read vault:write. Click "Authorize".
  3. GET /documents — you see documents up to clearance level 3.
  4. POST /documents with {"title": "Classified", "content": "...", "secret_level": 4} — you get 403 Forbidden (alice's clearance level is 3, which is below 4).
  5. POST /documents with secret_level: 2 — succeeds.
  6. GET /admin/users — fails for alice (missing admin scope).
  7. Log out, log in as admin with password admin123. GET /admin/users now succeeds.