FastAPI выводит почти всё из сигнатуры функции-обработчика. Объявил параметр с типом — получил его разбор, валидацию и документацию бесплатно. Это сильно сокращает шаблонный код, но требует понимать, как именно фреймворк решает, откуда взять каждый аргумент.
Path operations
Эндпоинт — это функция, помеченная декоратором метода на роутере: @router.get, @router.post и так далее. В FastAPI их называют path operations.
from fastapi import APIRouter
router = APIRouter(prefix="/products", tags=["products"])
@router.get("/{product_id}")
async def get_product(product_id: int):
...
prefix задаёт общий путь для всех роутов группы, tags группирует их в автодокументации. Это не косметика: на сервисе с десятком доменов префиксы и теги — то, что держит API читаемым.
Откуда берутся параметры
FastAPI определяет источник каждого аргумента по его типу и месту:
- если имя аргумента совпадает с частью пути (
{product_id}) — это параметр пути; - если аргумент — простой тип (
int,str,bool) и его нет в пути — это параметр запроса (query); - если аргумент — модель Pydantic — это тело запроса.
from typing import Annotated
from fastapi import Query
@router.get("/")
async def list_products(
category: str,
limit: Annotated[int, Query(ge=1, le=100)] = 20,
):
...
Annotated[int, Query(ge=1, le=100)] добавляет ограничения прямо в объявление — FastAPI проверит их до входа в функцию и вернёт понятную ошибку, если значение вне диапазона. То же делает Path(...) для параметров пути.
Тело запроса описывается моделью Pydantic — FastAPI разберёт JSON, провалидирует и передаст готовый объект:
@router.post("/", status_code=201)
async def create_product(body: CreateProductRequest):
...
Подробно про модели — в статье про Pydantic.
response_model и статус-коды
То, что эндпоинт возвращает наружу, стоит описывать явно через response_model (или возвращаемую аннотацию). FastAPI отфильтрует ответ по модели — лишние поля не утекут, — и опишет схему в документации.
@router.get("/{product_id}", response_model=ProductResponse)
async def get_product(product_id: int):
return await handler.handle(product_id)
Возврат разной модели на вход и на выход — не формальность, а защита: внутреннюю модель с техническими полями нельзя случайно отдать клиенту. status_code задаёт код успеха (201 на создание, 204 на удаление без тела); ошибки — через HTTPException или обработчики исключений.
Тонкий контроллер UCP
Соблазн FastAPI — написать всю логику прямо в функции-эндпоинте: фреймворк это позволяет, и для одной операции выглядит компактно. На сервисе это быстро превращается в роуты по двести строк с базой, правилами и ветвлением вперемешку.
В UCP роутер — это контроллер, и он тонкий: разобрать вход, вызвать Handler, отдать ответ. Никакой бизнес-логики.
@router.post("/", status_code=201, response_model=ProductResponse)
async def create_product(
body: CreateProductRequest,
handler: CreateProductHandlerDep,
) -> ProductResponse:
product = await handler.handle(body.to_command())
return ProductResponse.from_domain(product)
Эндпоинт здесь не знает ни про базу, ни про правила — только про HTTP. Сценарий — в Handler-е, собранном через зависимость. Это та же граница «контроллер тонкий», что и в Spring MVC, и именно она позволяет продукт-инженеру держать сервис понятным, когда роутов становится много.