Every HTTPWhat is http?The protocol browsers and servers use to exchange web pages, API data, and other resources, defining how requests and responses are formatted. request carries data in specific locations: the URL path, the query stringWhat is query string?The part of a URL after the ? that carries optional key-value pairs (like ?page=2&limit=10) used for filtering, sorting, or pagination., the headers, and the body. FastAPI uses Python type hints to declare where each piece of data comes from, and it handles parsing, validation, and documentation automatically. Understanding this flow is essential for reading and reviewing the endpointWhat is endpoint?A specific URL path on a server that handles a particular type of request, like GET /api/users. signatures AI generates.
Path parameters
Path parameters are dynamic segments in the URL. You declare them with curly braces in the path and as function parameters with type annotations.
@app.get("/users/{user_id}")
async def get_user(user_id: int):
return {"user_id": user_id}When a client requests GET /users/42, FastAPI extracts "42" from the URL, converts it to an int (because the type annotationWhat is type annotation?Explicitly labeling a variable or function parameter with its type in TypeScript (e.g., name: string). says int), and passes it to the function. If the client sends GET /users/abc, FastAPI returns a 422 Unprocessable Entity error automatically, "abc" is not a valid integer.
You can have multiple path parameters in a single endpointWhat is endpoint?A specific URL path on a server that handles a particular type of request, like GET /api/users.:
@app.get("/organizations/{org_id}/teams/{team_id}/members")
async def list_team_members(org_id: int, team_id: int):
return {"org_id": org_id, "team_id": team_id, "members": []}Path parameter order matters
When two routes overlap, FastAPI matches them in declaration order. This is a common source of bugs in AI-generated code:
# This route matches /users/me
@app.get("/users/me")
async def get_current_user():
return {"user": "current"}
# This route matches /users/42, /users/abc, etc.
@app.get("/users/{user_id}")
async def get_user(user_id: int):
return {"user_id": user_id}If you swap the order and put /users/{user_id} first, a request to /users/me would match the path parameter route, FastAPI would try to convert "me" to an int, and the request would fail with a validation error.
/users/me and /users/{user_id}, the fixed route (/users/me) must come first. AI does not consistently get this right.Query parameters
Any function parameter that is not a path parameter is automatically treated as a query parameter.
@app.get("/items")
async def list_items(skip: int = 0, limit: int = 10):
return {"skip": skip, "limit": limit}A request to GET /items?skip=20&limit=5 sets skip=20 and limit=5. A request to GET /items uses the defaults: skip=0 and limit=10.
Query parameters without defaults are required:
@app.get("/search")
async def search(q: str):
return {"query": q}GET /search without a q parameter returns a 422 error. GET /search?q=python works.
Optional query parameters
Use None as the default to make a parameter optional:
@app.get("/items")
async def list_items(
skip: int = 0,
limit: int = 10,
category: str | None = None,
):
result = {"skip": skip, "limit": limit}
if category:
result["category"] = category
return result| Request | skip | limit | category |
|---|---|---|---|
GET /items | 0 | 10 | None |
GET /items?skip=5 | 5 | 10 | None |
GET /items?category=tools | 0 | 10 | "tools" |
GET /items?skip=5&limit=20&category=tools | 5 | 20 | "tools" |
Request body
Request bodies use Pydantic models. When a function parameter has a Pydantic model type, FastAPI reads the JSONWhat is json?A text format for exchanging data between systems. It uses key-value pairs and arrays, and every programming language can read and write it. body, validates it against the model, and passes the validated object to your function.
from pydantic import BaseModel
class ItemCreate(BaseModel):
name: str
price: float
description: str | None = None
@app.post("/items", status_code=201)
async def create_item(item: ItemCreate):
return {"id": 1, **item.model_dump()}If the client sends {"name": "Widget", "price": 9.99}, FastAPI creates an ItemCreate instance with description=None (it is optional). If the client sends {"name": "Widget"} (missing price), FastAPI returns a 422 with a detailed error:
{
"detail": [
{
"type": "missing",
"loc": ["body", "price"],
"msg": "Field required",
"input": {"name": "Widget"}
}
]
}This error response tells the client exactly what went wrong: the field price in the body is required but was not provided. No manual validation code needed.
Combining path, query, and body
FastAPI distinguishes between data sources based on where parameters are declared:
@app.put("/items/{item_id}")
async def update_item(
item_id: int, # Path parameter (in the URL)
notify: bool = False, # Query parameter (after ?)
item: ItemCreate = None, # Body (JSON payload)
):
return {
"item_id": item_id,
"notify": notify,
"item": item.model_dump() if item else None,
}The rule is simple: if the parameter name appears in the path string ({item_id}), it is a path parameter. If the parameter type is a Pydantic model, it is a body parameter. Everything else is a query parameter.
skip, limit) as part of the request body instead of as query parameters. Another is using a path parameter for data that should be in the body (like sending user data in the URL). If an AI-generated endpoint signature looks odd, check whether each parameter is in the right place.Status codes
FastAPI sets status codes on the decorator, not on the response object:
@app.post("/items", status_code=201)
async def create_item(item: ItemCreate):
return {"id": 1, **item.model_dump()}
@app.delete("/items/{item_id}", status_code=204)
async def delete_item(item_id: int):
return None # 204 No Content - empty responseFor error responses, use HTTPException:
from fastapi import HTTPException
@app.get("/items/{item_id}")
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]| Status code | Meaning | When to use |
|---|---|---|
| 200 | OK | Successful GET, PUT, PATCH |
| 201 | Created | Successful POST that creates a resource |
| 204 | No Content | Successful DELETE |
| 400 | Bad Request | Client sent invalid data (when Pydantic validation is not enough) |
| 404 | Not Found | Resource does not exist |
| 422 | Unprocessable Entity | Pydantic validation failed (automatic) |
| 500 | Internal Server Error | Unhandled exception (automatic) |
200 for POST endpoints that create resources. The correct status code is 201 Created. This is a minor but telling mistake, it shows the AI is not thinking about HTTP semantics, just returning data. Always check the status_code argument on POST decorators.Response models
The response_model parameter controls what fields appear in the response and generates accurate documentation:
class UserIn(BaseModel):
name: str
email: str
password: str
class UserOut(BaseModel):
id: int
name: str
email: str
# password is NOT included
@app.post("/users", response_model=UserOut, status_code=201)
async def create_user(user: UserIn):
saved_user = save_to_db(user)
return saved_user # Even if saved_user has a password field, it's filtered outThe response_model filters the output to include only the fields defined in UserOut. Even if the internal saved_user object has a password attribute, it will not appear in the response. This is a security feature: it prevents accidental data leaks.
JSONResponse for custom responses
When you need full control over the response, custom headers, a specific media type, or a non-JSONWhat is json?A text format for exchanging data between systems. It uses key-value pairs and arrays, and every programming language can read and write it. body, use JSONResponse:
from fastapi.responses import JSONResponse
@app.get("/custom")
async def custom_response():
return JSONResponse(
content={"message": "Custom"},
status_code=200,
headers={"X-Custom-Header": "value"},
)Use JSONResponse sparingly. For most endpoints, returning a dictionary or Pydantic model is cleaner and keeps the auto-documentation accurate.