← назад к разделу

PYTHON · FASTAPI Фаза 5. Фундамент - ядро FastAPI

Обязательно

Роутинг и обработка запросов в FastAPI

Path · Query · Response model

По сигнатуре функции FastAPI выбирает маршрут, читает параметры, проверяет их и формирует публичный ответ.

1. Декоратор связывает метод, путь и функцию

1 / 5

@router.get("/{product_id}") объявляет path operation. APIRouter добавляет общий prefix и tags, а app.include_router собирает домены вместе.

GET /products/42HTTP request@router.getprefix=/productspath=/{product_id}get_productproduct_id=42

2. Источник параметра виден по сигнатуре

2 / 5

Имя из пути - path, простой тип вне пути - query, BaseModel - тело JSON. Header и Cookie объявляются явно.

Аргумент{id} → Pathint / str → QueryBaseModel → BodyHeader / CookieTyped valuesvalidated

3. Query и Path проверяют до обработчика

3 / 5

Annotated[int, Query(ge=1, le=100)] отсекает недопустимые значения. Клиент получает ошибку валидации, а функция не выполняется.

?limit=500raw queryQuery(le=100)500 > 100422handlerне вызван

4. response_model отсекает внутренние поля

4 / 5

Доменный объект может содержать internal_code и секреты, но наружу уйдут только поля ProductResponse. Аннотация результата даёт тот же контракт и проверку типов.

Domain Productid · name · priceinternal_codecost_priceProductResponsefilter + schemaJSONбез секретов

5. Статус и порядок маршрутов - часть API

5 / 5

Создание возвращает 201, удаление без тела - 204, отсутствие - 404 через HTTPException. Статический /users/me объявляют выше динамического /users/{id}.

1GET /users/mestatic first2GET /users/{user_id}dynamic secondиначе me попадёт в {user_id} и получит 422

Когда браузер или мобильное приложение делает 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. Фреймворк смотрит на имена и типы аргументов функции и сам решает, откуда их взять:

аргумент функции параметр пути имя совпадает с {переменной} query-параметр простой тип вне пути тело запроса модель Pydantic заголовок, cookie Header(), Cookie() явно

Источник параметра 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 и выбор сценария.

Что почитать дальше