Когда браузер или мобильное приложение делает HTTP-запрос, сервер должен понять: какую функцию вызвать и какие данные из запроса ей передать. Это и есть роутинг. В FastAPI он устроен так, что большую часть работы фреймворк делает за вас — по сигнатуре функции.
Как объявить маршрут
В Flask или обычном веб-сервере маршрут — это строка, к которой отдельно привязывают функцию. В FastAPI они объединены: декоратор на функции сразу задаёт и метод, и путь.
from fastapi import FastAPI
app = FastAPI()
@app.get("/hello")
async def say_hello():
return {"message": "Hello"}
Такой эндпоинт в FastAPI называется path operation: path — путь (/hello), operation — HTTP-метод (GET). Есть декораторы для всех стандартных методов: @app.get, @app.post, @app.put, @app.patch, @app.delete.
APIRouter — порядок в большом API
Если все маршруты описывать прямо в app, файл быстро разрастается. APIRouter позволяет разбить API на модули и собрать их в одном месте.
# products/router.py
from fastapi import APIRouter
router = APIRouter(prefix="/products", tags=["products"])
@router.get("")
async def list_products():
...
@router.get("/{product_id}")
async def get_product(product_id: int):
...
# main.py
from fastapi import FastAPI
from products.router import router as products_router
app = FastAPI()
app.include_router(products_router)
prefix добавляет общее начало пути ко всем маршрутам роутера — не нужно повторять /products в каждом декораторе. tags группирует маршруты в автоматической документации (/docs). Корень роутера объявляют пустым путём "": при "/" адрес получился бы /products/ со слешем на конце, и запрос на /products уходил бы через редирект.
Откуда берутся параметры функции
Это главная «магия» FastAPI. Фреймворк смотрит на имена и типы аргументов функции и сам решает, откуда их взять:
Источник параметра FastAPI определяет по сигнатуре: совпал с путём, простой тип, модель или явная пометка.
Параметр пути — имя аргумента совпадает с {переменной} в пути:
@router.get("/{product_id}")
async def get_product(product_id: int):
# product_id берётся из URL: /products/42 → product_id = 42
...
FastAPI не просто достаёт строку из URL — он сразу преобразует её в нужный тип и валидирует. Если передать /products/abc, а тип int — клиент получит понятную ошибку до входа в функцию.
Параметр запроса — простой тип (str, int, bool), которого нет в пути. Берётся из строки запроса (?key=value):
@router.get("")
async def list_products(category: str = "all", limit: int = 20):
# GET /products?category=electronics&limit=10
...
Параметры со значением по умолчанию — необязательные. Без значения — обязательные.
Тело запроса — аргумент с типом-моделью Pydantic. FastAPI читает JSON из тела запроса, разбирает его и передаёт готовый объект:
from pydantic import BaseModel
class CreateProductRequest(BaseModel):
name: str
price: float
@router.post("")
async def create_product(body: CreateProductRequest):
# body.name, body.price уже провалидированы
...
Подробно про модели Pydantic — в статье Pydantic: валидация и сериализация.
Дополнительные ограничения через Query и Path
Иногда нужно не просто получить параметр, но и ограничить его допустимые значения. Для этого используют Query и Path внутри Annotated:
from typing import Annotated
from fastapi import Query, Path
@router.get("/{product_id}")
async def get_product(
product_id: Annotated[int, Path(ge=1)],
limit: Annotated[int, Query(ge=1, le=100)] = 20,
):
...
ge=1 — «больше или равно 1», le=100 — «меньше или равно 100». FastAPI проверит эти условия и вернёт ошибку, если значение выходит за границы. Это работает до входа в функцию — не нужно писать проверки вручную.
response_model — что отдать клиенту
По умолчанию FastAPI вернёт то, что вернула функция, сериализовав в JSON. Но это не всегда безопасно: внутренняя модель может содержать поля, которые не нужно отдавать наружу (пароли, внутренние идентификаторы).
response_model решает это: FastAPI отфильтрует ответ по указанной модели — лишние поля не попадут в ответ — и опишет схему в документации.
class ProductResponse(BaseModel):
id: int
name: str
price: float
@router.get("/{product_id}", response_model=ProductResponse)
async def get_product(product_id: int):
return await service.get_by_id(product_id)
# даже если service вернёт объект с полем internal_code — клиент его не увидит
Статус-коды
По умолчанию успешный ответ возвращается с кодом 200. Для других случаев статус задаётся в декораторе:
@router.post("", status_code=201) # создание → 201 Created
async def create_product(body: CreateProductRequest):
...
@router.delete("/{product_id}", status_code=204) # удаление → 204 No Content
async def delete_product(product_id: int):
...
Ошибки возвращают через HTTPException:
from fastapi import HTTPException
@router.get("/{product_id}")
async def get_product(product_id: int):
product = await service.find(product_id)
if product is None:
raise HTTPException(status_code=404, detail="Продукт не найден")
return product
Подробнее об обработке ошибок — в статье Middleware и обработка ошибок.
Глубже: тип ответа в сигнатуре, порядок маршрутов, заголовки и спискирасширенное
Современный способ задать модель ответа — аннотация возвращаемого типа: async def get_product(...) -> ProductResponse.
FastAPI поступит с ней так же, как с response_model: отфильтрует лишние поля, опишет схему
в документации, а анализатор типов заодно проверит, что функция возвращает именно это.
Порядок объявления маршрутов важен. Если /users/{user_id} с типом int объявлен раньше
/users/me, запрос GET /users/me попадёт в первый маршрут и вернёт 422: «me» не число.
Статичные пути ставят выше динамических.
Параметры берут не только из пути и строки запроса. Заголовок и cookie объявляют явно, а имя
FastAPI переводит сам: x_request_id станет заголовком X-Request-ID:
from fastapi import Cookie, Header
@router.get("/whoami")
async def whoami(
x_request_id: Annotated[str | None, Header()] = None,
session_id: Annotated[str | None, Cookie()] = None,
):
...
Список в строке запроса (?ids=1&ids=2) требует явного Query(): ids: Annotated[list[int], Query()] = [],
иначе FastAPI решит, что список — это тело запроса. Параметр пути можно ограничить перечислением
через Enum: тогда и проверка, и список допустимых значений в документации появятся сами.
Коротко
- Маршрут объявляют декоратором:
@router.get("/path"),@router.post("/path")и т.д. APIRouterсprefixиtagsразбивает API на модули — каждый домен в своём файле.- FastAPI сам определяет источник параметра: совпадает с
{переменной}в пути — параметр пути; простой тип вне пути — query-параметр; модель Pydantic — тело запроса. - Параметры автоматически преобразуются в нужный тип и валидируются до входа в функцию.
Query(ge=1, le=100)иPath(ge=1)добавляют ограничения без ручных проверок.response_modelфильтрует ответ — лишние поля не утекают клиенту.- Статус-код успеха задают в декораторе (
status_code=201), ошибки — черезHTTPException. - Тип ответа задают аннотацией
-> ProductResponse; статичные пути объявляют выше/{id}; заголовки и cookie — черезHeader()иCookie(), списки в query — через явныйQuery().
Что пощупать
Роутер каталога в практикуме remodov/marketplace-system-python: APIRouter с префиксом, status_code=201 у создания, путь /{id} с типом uuid.UUID, причём мусор в пути получает 400 в формате Problem Details, а не 422 по умолчанию - за это отвечает обработчик RequestValidationError.
Код: router.py, problem.py.
Сделаем сами
Ветка step-02-read-endpoint - добавить параметр maxPrice и выбор сценария.
Что почитать дальше
- Dependency Injection: Depends — как передавать зависимости в эндпоинты.
- Pydantic: валидация и сериализация — модели для тела запроса и ответа.
- Middleware и обработка ошибок — глобальная обработка исключений.