Routes and Parameters
20 min · Intermediate
Need help? Ask a tutor on WhatsApp (opens in a new tab)An API is a set of addresses that programs call. In FastAPI each address is a function with a decorator. FastAPI reads the path, the query string and the body for you and checks every value before your code runs.
A route joins a method and a path to a function
Route is one HTTP method plus one URL path, tied to the function that answers it. FastAPI builds routes from decorators such as @app.get and @app.post.
FastAPI is a Python framework for building web APIs. You write a normal function and put a decorator above it. The decorator says which HTTP method and which path should call the function.
@app.get("/products/{product_id}")def read_product(product_id: int): return {"id": product_id}Example 1 · A route and a bad path value
from fastapi import FastAPIfrom fastapi.testclient import TestClient
app = FastAPI()products = {1: "Keyboard", 2: "Monitor"}
@app.get("/products/{product_id}")def read_product(product_id: int): return {"id": product_id, "name": products[product_id]}
client = TestClient(app)response = client.get("/products/2")print(response.status_code)print(response.json())print(client.get("/products/abc").status_code)200
{'id': 2, 'name': 'Monitor'}
422What this code does
TestClientcalls the app directly, so no server is needed.- The text
abcis not an int, so FastAPI answers 422 before the function runs.
A GET request to /products/5 calls the function with product_id=5. FastAPI turns the returned dict into JSON.
Path and query parameters with limits
Query parameter is a named value after the ? in a URL, such as ?limit=10. A function argument that is not in the path and is not a model is read as a query parameter.
A path parameter is a part of the URL path, in braces. A query parameter is a value after the ?. Both come from the function arguments. Add Path(...) or Query(...) inside Annotated to set limits such as ge (greater or equal) and le (less or equal).
from typing import Annotatedfrom fastapi import Path, Query
@app.get("/orders/{order_id}")def read_order( order_id: Annotated[int, Path(ge=1)], limit: Annotated[int, Query(ge=1, le=50)] = 10,): return {"order_id": order_id, "limit": limit}Example 2 · Limits on path and query values
from typing import Annotatedfrom fastapi import FastAPI, Path, Queryfrom fastapi.testclient import TestClient
app = FastAPI()
@app.get("/orders/{order_id}")def read_order( order_id: Annotated[int, Path(ge=1)], status: Annotated[str, Query(max_length=10)] = "open", limit: Annotated[int, Query(ge=1, le=50)] = 10,): return {"order_id": order_id, "status": status, "limit": limit}
client = TestClient(app)print(client.get("/orders/7?limit=5").json())bad = client.get("/orders/7?limit=500").json()["detail"][0]print(bad["loc"], bad["msg"])bad = client.get("/orders/0").json()["detail"][0]print(bad["loc"], bad["msg"]){'order_id': 7, 'status': 'open', 'limit': 5}
['query', 'limit'] Input should be less than or equal to 50
['path', 'order_id'] Input should be greater than or equal to 1What this code does
- The defaults fill in
statusandlimitwhen the client leaves them out. - The
locfield says where the bad value was:queryorpath.
Example 3 · A list in the query string
from typing import Annotatedfrom fastapi import FastAPI, Queryfrom fastapi.testclient import TestClient
app = FastAPI()catalog = [ {"name": "Keyboard", "tag": "office"}, {"name": "Monitor", "tag": "office"}, {"name": "Router", "tag": "network"},]
@app.get("/products")def list_products(tag: Annotated[list[str] | None, Query()] = None): if not tag: return catalog return [p for p in catalog if p["tag"] in tag]
client = TestClient(app)print(len(client.get("/products").json()))print([p["name"] for p in client.get("/products?tag=network").json()])print(len(client.get("/products?tag=network&tag=office").json()))3 ['Router'] 3
What this code does
- Repeating a name (
tag=a&tag=b) fills a list when the type islist[str]. - Without
Query()FastAPI would not treat a list as a query parameter.
An argument with a default value is an optional query parameter. An argument without a default is required. A value outside the limits is rejected before your function runs.
The body comes from a model
A body is the data a client sends inside a POST or PUT request, usually as JSON. Declare an argument whose type is a Pydantic model and FastAPI reads the body into it. You can mix a path parameter and a body in the same function.
from pydantic import BaseModel
class CustomerIn(BaseModel): name: str city: str
@app.put("/customers/{customer_id}")def update_customer(customer_id: int, customer: CustomerIn): return {"id": customer_id, **customer.model_dump()}Example 4 · Path and body in one route
from fastapi import FastAPIfrom fastapi.testclient import TestClientfrom pydantic import BaseModel
app = FastAPI()
class CustomerIn(BaseModel): name: str city: str
@app.put("/customers/{customer_id}")def update_customer(customer_id: int, customer: CustomerIn): return {"id": customer_id, **customer.model_dump()}
client = TestClient(app)r = client.put("/customers/4", json={"name": "Asha", "city": "Pune"})print(r.status_code, r.json())r = client.put("/customers/4", json={"name": "Asha"})print(r.status_code, r.json()["detail"][0]["loc"])200 {'id': 4, 'name': 'Asha', 'city': 'Pune'}
422 ['body', 'city']What this code does
json=sends the body and sets the content type for you.- A missing field is reported with its place:
bodythen the field name.
customer_id comes from the path and customer comes from the JSON body. model_dump() turns the model back into a dict.
Choose the status code on purpose
A status code is the number in the response that tells the client what happened. Set a success code with status_code= on the decorator: 201 for created, 204 for no content. For a failure, raise HTTPException with the code you want. Put fixed paths such as /orders/latest above /orders/{order_id}, because FastAPI checks routes in the order you wrote them.
@app.post("/orders", status_code=201)def create_order(order: OrderIn): return order
@app.get("/orders/{order_id}")def read_order(order_id: int): if order_id not in orders: raise HTTPException(status_code=404, detail="Order not found")Example 5 · Status codes and fixed routes first
from fastapi import FastAPI, HTTPExceptionfrom fastapi.testclient import TestClientfrom pydantic import BaseModel
app = FastAPI()orders = {1: "Keyboard", 2: "Monitor"}
class OrderIn(BaseModel): item: str
@app.post("/orders", status_code=201)def create_order(order: OrderIn): new_id = max(orders) + 1 orders[new_id] = order.item return {"id": new_id, "item": order.item}
@app.get("/orders/latest")def latest_order(): last = max(orders) return {"id": last, "item": orders[last]}
@app.get("/orders/{order_id}")def read_order(order_id: int): if order_id not in orders: raise HTTPException(status_code=404, detail="Order not found") return {"id": order_id, "item": orders[order_id]}
@app.delete("/orders/{order_id}", status_code=204)def delete_order(order_id: int): orders.pop(order_id, None)
client = TestClient(app)print(client.post("/orders", json={"item": "Router"}).status_code)print(client.get("/orders/latest").json())r = client.get("/orders/99")print(r.status_code, r.json())r = client.delete("/orders/1")print(r.status_code, repr(r.content))201
{'id': 3, 'item': 'Router'}
404 {'detail': 'Order not found'}
204 b''What this code does
/orders/latestis written before/orders/{order_id}, so it is not read as an id.- A 204 response has no body, so the content is empty bytes.
Use 404 for a missing record and 201 for a new record. The client can then act on the number without reading the text.
Next step
Check your understanding
12 quick questions