FastAPI/
Lesson

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.

AI pitfall
AI often generates path parameter routes before fixed routes with the same prefix. If you have /users/me and /users/{user_id}, the fixed route (/users/me) must come first. AI does not consistently get this right.
02

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
Requestskiplimitcategory
GET /items010None
GET /items?skip=5510None
GET /items?category=tools010"tools"
GET /items?skip=5&limit=20&category=tools520"tools"
03

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:

json
{
  "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.

04

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.

AI pitfall
AI sometimes puts data in the wrong location. A common mistake is declaring pagination parameters (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.
05

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 response

For 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 codeMeaningWhen to use
200OKSuccessful GET, PUT, PATCH
201CreatedSuccessful POST that creates a resource
204No ContentSuccessful DELETE
400Bad RequestClient sent invalid data (when Pydantic validation is not enough)
404Not FoundResource does not exist
422Unprocessable EntityPydantic validation failed (automatic)
500Internal Server ErrorUnhandled exception (automatic)
AI pitfall
AI frequently uses 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.
06

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 out

The 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.