---
config:
look: handDrawn
layout: elk
flowchart:
nodeSpacing: 20
rankSpacing: 30
padding: 15
---
flowchart TD
A[HTTP Request] --> B[[Path & Query Parameter Extraction]]
B --> C[[Request Body<br/>Validation]]
C --> D{{Route Function Executes}}
D --> E[[Response Model<br/>Validation]]
E --> F[JSON Response<br/>sent to Client]
style A fill:#e8dcc4,stroke:#000
style B fill:#fdf1b8,stroke:#000
style C fill:#fdf1b8,stroke:#000
style D fill:#d8e2dc,stroke:#000
style E fill:#fdf1b8,stroke:#000
style F fill:#e8dcc4,stroke:#000
2 From URL Paths to Python Parameters
Remember how Python functions work? Some parameters are required, some have defaults, and you validate what gets passed in. FastAPI endpoints work exactly the same way: the URL just becomes another way to pass arguments to your function.
To demonstrate how FastAPI works with URL paths, we will build a simple “Quote of the Day” API along the way.
2.1 Path Parameters: Required Values Inside the URL Path
When you define a Python function, positional arguments are required and ordered:
def get_quote(quote_id):
return quotes[quote_id]In FastAPI, you declare the same requirement in the URL path by wrapping the variable in curly braces. This is called a path parameter, For example:
@app.get("/quotes/{quote_id}")
def get_quote(quote_id: int):
return {"quote_id": quote_id, "message": "Here is your quote"}When someone visits, say, your-api-url/quotes/42, FastAPI automatically: - Extracts the value of the path parameter from the URL (in this case, 42) - Converts it to an integer (because of the int type hint) - Passes it as the quote_id argument to your get_quote function
The path parameter is mandatory (it is part of the URL path) and the value must be convertible to the specified type: - If someone tries to access /quotes/ without providing a quote_id, they’ll get a 404 error since that URL doesn’t match any defined route. - If they try /quotes/abc, FastAPI returns a friendly 422 Unprocessable Entity.
2.2 Query Parameters: Optional URL Inputs with Defaults
Just like Python functions can have optional keyword arguments with default values, FastAPI allows you to define query parameters that are passed in the URL after a question mark (?), e.g. /quotes?limit=20&author=Shakespeare.
Declare them in FastAPI by including them in the function signature but not in the path string:
@app.get("/quotes")
def list_quotes(limit: int = 10, author: str | None = None):
return {"limit": limit, "filter_by": author}If the client provides ?limit=20, your function receives 20. If they omit it, you get your default of 10.
The pipe syntax str | None (or Optional[str] in older Python) tells FastAPI this parameter is optional.
Can you write author: str = None without the | None? Yes, but it’s less explicit. The | None makes it clear that None is an acceptable value, while just str = None could be misinterpreted as a string that defaults to the literal “None”. Using str | None is more idiomatic in modern Python and FastAPI code.
Query parameters are perfect for searching, filtering, and pagination.
2.3 Request Bodies: When Input Belongs in JSON, Not the URL
Path and query parameters work for simple strings and numbers, but what if you need to pass a more complex structure, like dictionary with multiple fields? You don’t want to cram that into the URL. Instead, you send it as JSON in the request body.
In Python, you might define a function that takes a dictionary or a dataclass as an argument. In FastAPI, you use Pydantic models to define the expected structure of the incoming JSON. Pydantic models are like Python dataclasses but with built-in validation and parsing.
For example, if you want to create a new quote with a text and an author:
from pydantic import BaseModel
class QuoteCreate(BaseModel):
text: str
author: str
tags: list[str] = []Now you can define an endpoint that accepts this model as the request body:
@app.post("/quotes")
def create_quote(quote: QuoteCreate):
return {"stored": quote.text, "by": quote.author}When a client sends a POST request with JSON like {"text": "Be curious", "author": "Anonymous"}: - FastAPI automatically parses the JSON, validates that it has the required fields (text and author), that text and author are strings, and that tags is a list of strings (if provided). - If the client forgets the author field, they receive a detailed 422 error pointing exactly to what’s missing.
2.4 Response Models: Controlling Exactly What Your API Returns
Like request bodies, you can define a response model using Pydantic. This tells FastAPI: “No matter what happens inside this function, the outside world only sees this structure.”
from pydantic import BaseModel
class Quote(BaseModel):
id: int
text: str
author: str
@app.get("/quotes/{quote_id}", response_model=Quote)
def get_quote(quote_id: int):
# Even if this dictionary had extra keys like "internal_note",
# FastAPI would strip them and return only id, text, and author
return {
"id": quote_id,
"text": "The journey of a thousand miles...",
"author": "Lao Tzu"
}2.5 HTTPException: Turning Python Errors into HTTP Status Codes
In a Python script, when something goes wrong, you raise an exception and the program crashes or catches it. In a web API, you can’t crash—you must send a polite letter explaining the problem so the client can react appropriately.
FastAPI provides HTTPException to convert Python errors into proper HTTP status codes:
from fastapi import HTTPException
@app.get("/quotes/{quote_id}")
def get_quote(quote_id: int):
if quote_id not in quotes_db:
raise HTTPException(status_code=404, detail="Quote not found")
return quotes_db[quote_id]Now, instead of a mysterious server crash (status 500), the client receives a clean 404 Not Found with the message “Quote not found.”
You can use different codes for different situations: - 400 for “you sent bad data” (like a ValueError) - 401/403 for authentication issues (like a permission error) - 422 for validation failures (handled by Pydantic)
2.6 The Request Lifecycle: A Visual Map
Let’s visualize exactly what happens when your FastAPI app receives a request. This flow happens for every single endpoint call.
Validation acts as a safety net on both sides of your code: FastAPI ensures the inputs match your Python type hints and the output matches your response model. You focus on the function logic in the middle while FastAPI handles the wire protocol.
2.7 Hands-On: A Quote of the Day API
Let’s wire everything together into a working mini-application. We’ll build a “Quote of the Day” service that stores quotes in a Python dictionary and exposes a clean “REST” API (i.e, a web API that follows standard HTTP conventions).
Start fresh with a new file, quotes_app.py:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
# Our "database": a dictionary in memory
quotes_db = {}
next_id = 1
# Pydantic models for type safety
class QuoteCreate(BaseModel):
text: str
author: str
class Quote(BaseModel):
id: int
text: str
author: str
# Endpoint to create a new quote
@app.post("/quotes", response_model=Quote)
def create_quote(quote_data: QuoteCreate):
global next_id
new_quote = {
"id": next_id,
"text": quote_data.text,
"author": quote_data.author
}
quotes_db[next_id] = new_quote
next_id += 1
return new_quote
# Endpoint to retrieve a quote by ID
@app.get("/quotes/{quote_id}", response_model=Quote)
def get_quote(quote_id: int):
if quote_id not in quotes_db:
raise HTTPException(status_code=404, detail=f"Quote {quote_id} not found")
return quotes_db[quote_id]
# Endpoint to list quotes with pagination
@app.get("/quotes")
def list_quotes(limit: int = 10, skip: int = 0):
all_quotes = list(quotes_db.values())
return all_quotes[skip : skip + limit]Test this in your Swagger UI (/docs): 1. POST to /quotes with {"text": "Short cuts make long delays.", "author": "JRR Tolkien"}. Note the returned ID. 2. POST another quote to see the auto-incrementing IDs. 3. GET /quotes/1 to retrieve your first quote by its path parameter. 4. GET /quotes/1 with a non-existent ID to see your 404 error in action. 5. GET /quotes?limit=1 to test the query parameter filtering.
2.8 Recap: Terms You Learned
| Web Term | What it actually means in Python |
|---|---|
| Path Parameter | A positional argument extracted from the URL path, like /items/{item_id} mapping to def get_item(item_id) |
| Query Parameter | A keyword argument with a default value, extracted from the URL query string like ?limit=10 |
| Request Body | A Pydantic model (like a dataclass) shipped as JSON in the POST request, automatically validated |
| Response Model | The output type hint that guarantees the shape of your returned JSON, filtering extra fields |
| HTTPException | The bridge between Python exceptions and HTTP status codes; raise it to send 404, 400, etc. |
| Validation | The automatic checking that incoming data matches your Pydantic types, returning 422 if it fails |