FastAPI Routes and Pydantic Validation
30 min · Intermediate
Need help? Ask a tutor on WhatsApp (opens in a new tab)FastAPI is a Python package for building web APIs. You write ordinary functions and add a line that says which method and path they answer. FastAPI reads the request, checks it and sends the JSON reply for you.
A route is a function with an address
A route connects one HTTP method and path, such as GET /health, to one Python function. When a matching request arrives, FastAPI calls the function and sends back its return value as JSON.
A decorator above a function names the method and the path it answers. Whatever the function returns, a dictionary or a list, FastAPI sends as JSON.
from fastapi import FastAPI
app = FastAPI()
@app.get("/health")def health(): return {"status": "ok"}Save this as main.py, then run python -m uvicorn main:app --reload in the terminal. Open http://127.0.0.1:8000/docs for a page that lists every route and lets you try it.
Path and query parameters
A path parameter is a part of the URL that changes, written in braces in the route, such as /products/{product_id}. It picks one record. Query parameters after the ? filter or shape a list.
A name in curly braces, like {product_id}, is a path parameter. Any other argument is a query parameter. The type hint decides how the text is converted and checked.
@app.get("/products/{product_id}")def get_product(product_id: int): return {"id": product_id}
@app.get("/products")def list_products(max_price: float | None = None, limit: int = 10): return {"max_price": max_price, "limit": limit}Example 1 · A route and a path parameter
from fastapi import FastAPIfrom fastapi.testclient import TestClient
app = FastAPI()
@app.get("/health")def health(): return {"status": "ok"}
@app.get("/products/{product_id}")def get_product(product_id: int): return {"id": product_id, "name": "Notebook"}
client = TestClient(app)print(client.get("/health").json())print(client.get("/products/7").json())response = client.get("/products/abc")print(response.status_code)print(response.json()["detail"][0]["msg"]){'status': 'ok'}
{'id': 7, 'name': 'Notebook'}
422
Input should be a valid integer, unable to parse string as an integerWhat this code does
{product_id}in the path becomes the function argument. The typeintmakes FastAPI turn the text7into the number 7.- The text
abccannot become an integer, so FastAPI answers422before your function runs. TestClientcalls the app in memory, so you see the real answers without starting a server.
Example 2 · Optional query parameters
from fastapi import FastAPI, Queryfrom fastapi.testclient import TestClient
app = FastAPI()PRODUCTS = [ {"id": 1, "name": "Notebook", "price": 120.0}, {"id": 2, "name": "Pen", "price": 25.0}, {"id": 3, "name": "Backpack", "price": 899.0},]
@app.get("/products")def list_products(max_price: float | None = None, limit: int = Query(10, ge=1, le=50)): items = [p for p in PRODUCTS if max_price is None or p["price"] <= max_price] return items[:limit]
client = TestClient(app)print(len(client.get("/products").json()))print(client.get("/products?max_price=100").json())response = client.get("/products?limit=0")print(response.status_code, response.json()["detail"][0]["msg"])3
[{'id': 2, 'name': 'Pen', 'price': 25.0}]
422 Input should be greater than or equal to 1What this code does
- A parameter that is not in the path is a query parameter.
max_price=Nonemakes it optional. Query(10, ge=1, le=50)gives a default of 10 and allows values from 1 to 50 only.- A value outside the range is refused with
422and a clear message.
A request to /products?max_price=100 fills max_price with 100.0. An argument with a default value is optional; one without a default is required.
Pydantic checks the request body
A Pydantic model is a class that lists the fields of some data and their rules. It converts safe values, such as "15" to 15.0, and raises a clear error for anything that breaks a rule.
For POST and PATCH the data arrives as JSON. Describe it once as a Pydantic model. FastAPI checks every field before your function runs and refuses bad data with a clear error.
from pydantic import BaseModel, Field
class ProductIn(BaseModel): name: str = Field(min_length=2, max_length=60) price: float = Field(gt=0)
@app.post("/products", status_code=201)def create_product(data: ProductIn): return {"id": 1, **data.model_dump()}Example 3 · Validation with a Pydantic model
from fastapi import FastAPIfrom fastapi.testclient import TestClientfrom pydantic import BaseModel, Field
app = FastAPI()
class ProductIn(BaseModel): name: str = Field(min_length=2, max_length=60) price: float = Field(gt=0)
@app.post("/products")def create_product(data: ProductIn): return {"id": 1, **data.model_dump()}
client = TestClient(app)print(client.post("/products", json={"name": "Ruler", "price": "15"}).json())response = client.post("/products", json={"name": "R", "price": -1})print(response.status_code)for problem in response.json()["detail"]: print(problem["loc"][-1], "-", problem["msg"]){'id': 1, 'name': 'Ruler', 'price': 15.0}
422
name - String should have at least 2 characters
price - Input should be greater than 0What this code does
ProductInlists the fields and rules. FastAPI reads the JSON body, builds the model and checks every rule.- The text
"15"is converted to the number15.0. Pydantic converts safe values and refuses the rest. - Both mistakes are reported together, each with the field name in
loc.
A function argument typed with a model is read from the body. model_dump() turns the checked model back into a plain dictionary. Bad input never reaches the function, because FastAPI answers 422 first.
Choose the status code on purpose
HTTPException is an exception you raise inside a route to stop it and send an error response. You give it a status code and a detail text, and FastAPI turns it into JSON for the client.
Success is 200 by default. A POST that creates something should say 201. When a record is missing, raise HTTPException with 404, so the client knows exactly what went wrong.
from fastapi import HTTPException
@app.get("/products/{product_id}")def get_product(product_id: int): product = PRODUCTS.get(product_id) if product is None: raise HTTPException(status_code=404, detail="Product not found") return productExample 4 · Created and not found
from fastapi import FastAPI, HTTPExceptionfrom fastapi.testclient import TestClientfrom pydantic import BaseModel
app = FastAPI()PRODUCTS = {1: {"id": 1, "name": "Notebook"}}
class ProductIn(BaseModel): name: str
@app.post("/products", status_code=201)def create_product(data: ProductIn): new_id = max(PRODUCTS) + 1 PRODUCTS[new_id] = {"id": new_id, "name": data.name} return PRODUCTS[new_id]
@app.get("/products/{product_id}")def get_product(product_id: int): if product_id not in PRODUCTS: raise HTTPException(status_code=404, detail="Product not found") return PRODUCTS[product_id]
client = TestClient(app)response = client.post("/products", json={"name": "Pen"})print(response.status_code, response.json())response = client.get("/products/9")print(response.status_code, response.json())201 {'id': 2, 'name': 'Pen'}
404 {'detail': 'Product not found'}What this code does
status_code=201on the decorator changes the success code from 200 to 201.raise HTTPException(...)stops the function and sends the status code with a JSONdetail.- The dictionary stands in for a database here. The next lesson replaces it with real tables.
The client gets {"detail": "Product not found"} with status 404. Keep error texts short and free of secrets or internal details.
Next step
Check your understanding
10 quick questions