Skip to main content
Advertisement

Routers and Path Parameters

APIRouter lets you split endpoints into separate modules. You can also fine-tune path, query, body, and header parameters.


APIRouter — Splitting Routes

# routers/users.py
from fastapi import APIRouter, HTTPException, status
from pydantic import BaseModel

router = APIRouter(
prefix="/users", # prepend /users to all paths
tags=["users"], # Swagger UI grouping
)


class User(BaseModel):
id: int
name: str
email: str


fake_users: dict[int, User] = {
1: User(id=1, name="Alice", email="alice@example.com"),
2: User(id=2, name="Bob", email="bob@example.com"),
}


@router.get("/", response_model=list[User])
def list_users():
return list(fake_users.values())


@router.get("/{user_id}", response_model=User)
def get_user(user_id: int):
if user_id not in fake_users:
raise HTTPException(status_code=404, detail="User not found")
return fake_users[user_id]


@router.post("/", response_model=User, status_code=status.HTTP_201_CREATED)
def create_user(user: User):
fake_users[user.id] = user
return user


@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_user(user_id: int):
if user_id not in fake_users:
raise HTTPException(status_code=404, detail="User not found")
del fake_users[user_id]
# main.py
from fastapi import FastAPI
from routers import users, items

app = FastAPI(title="My API")

app.include_router(users.router)
app.include_router(items.router)

# Add version prefix
# app.include_router(users.router, prefix="/api/v1")

Path Parameters

from fastapi import FastAPI, Path
from typing import Annotated
from enum import Enum

app = FastAPI()


# Type validation is automatic
@app.get("/items/{item_id}")
def get_item(item_id: int): # str → int conversion + validation
return {"item_id": item_id}


# Additional validation with Path()
@app.get("/products/{product_id}")
def get_product(
product_id: Annotated[int, Path(ge=1, le=9999, description="Product ID")]
):
return {"product_id": product_id}


# Restrict values with Enum
class ModelName(str, Enum):
alexnet = "alexnet"
resnet = "resnet"
lenet = "lenet"


@app.get("/models/{model_name}")
def get_model(model_name: ModelName):
if model_name == ModelName.alexnet:
return {"model": model_name, "message": "Deep Learning"}
return {"model": model_name}


# File path (includes slashes)
@app.get("/files/{file_path:path}")
def read_file(file_path: str):
return {"file_path": file_path}
# GET /files/home/user/doc.txt → file_path = "home/user/doc.txt"

Query Parameters

from fastapi import FastAPI, Query
from typing import Annotated

app = FastAPI()


# Query with default values
@app.get("/items/")
def list_items(skip: int = 0, limit: int = 10):
return {"skip": skip, "limit": limit}
# GET /items/?skip=20&limit=5


# Required query (no default)
@app.get("/search/")
def search(q: str):
return {"q": q}


# Validation with Query()
@app.get("/advanced/")
def advanced_search(
q: Annotated[str | None, Query(
min_length=2,
max_length=50,
pattern=r"^[a-zA-Z\s]+$",
description="Search term (letters and spaces only)",
)] = None,
page: Annotated[int, Query(ge=1)] = 1,
size: Annotated[int, Query(ge=1, le=100)] = 20,
):
return {"q": q, "page": page, "size": size}


# List query
@app.get("/items/filter/")
def filter_items(
tags: Annotated[list[str], Query()] = [],
):
return {"tags": tags}
# GET /items/filter/?tags=python&tags=fastapi

Request Body

from fastapi import FastAPI, Body
from pydantic import BaseModel, Field
from typing import Annotated

app = FastAPI()


class Item(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
description: str | None = Field(None, max_length=300)
price: float = Field(..., gt=0)
tax: float | None = None


class User(BaseModel):
username: str
full_name: str | None = None


# Single body
@app.post("/items/")
def create_item(item: Item):
item_dict = item.model_dump()
if item.tax:
item_dict["price_with_tax"] = item.price + item.tax
return item_dict


# Multiple bodies (nested JSON)
@app.put("/items/{item_id}")
def update_item(
item_id: int,
item: Item,
user: User,
importance: Annotated[int, Body(ge=1, le=5)] = 1,
):
return {
"item_id": item_id,
"item": item,
"user": user,
"importance": importance,
}
# Request JSON:
# {
# "item": {"name": "Foo", "price": 10.5},
# "user": {"username": "alice"},
# "importance": 3
# }

Headers, Cookies, and Form Parameters

from fastapi import FastAPI, Header, Cookie, Form
from typing import Annotated

app = FastAPI()


# Headers
@app.get("/headers/")
def read_headers(
user_agent: Annotated[str | None, Header()] = None,
x_token: Annotated[str | None, Header(alias="X-Token")] = None,
):
return {"User-Agent": user_agent, "X-Token": x_token}


# Cookies
@app.get("/cookies/")
def read_cookies(
session_id: Annotated[str | None, Cookie()] = None,
):
return {"session_id": session_id}


# Form data (application/x-www-form-urlencoded)
@app.post("/login/")
def login(
username: Annotated[str, Form()],
password: Annotated[str, Form()],
):
return {"username": username}
# Note: Form and JSON Body cannot be used simultaneously


# File upload
from fastapi import UploadFile, File


@app.post("/upload/")
async def upload_file(
file: Annotated[UploadFile, File(description="Upload file")],
):
contents = await file.read()
return {
"filename": file.filename,
"content_type": file.content_type,
"size": len(contents),
}

Summary

ParameterClassLocation
PathPath()URL path /{id}
QueryQuery()After URL ?key=value
BodyBody() / PydanticRequest body (JSON)
HeaderHeader()HTTP headers
CookieCookie()Cookie header
FormForm()form-data
FileUploadFile + File()multipart

APIRouter makes large APIs maintainable by separating endpoints into feature-based files.

Advertisement