Параметры пути и их валидация

Path-параметры — это переменные части URL вроде /users/42, которые FastAPI извлекает по типу аннотации, конвертирует и валидирует.

Объявив в пути /items/{item_id} и параметр item_id: int, вы получаете автоматическое преобразование "42" в число и ошибку 422, если в URL пришло не число.

Путь запроса часто несёт идентификатор ресурса: номер пользователя, slug статьи, код товара. В FastAPI вы помечаете изменяемую часть фигурными скобками в строке маршрута и принимаете её одноимённым параметром функции. Тип параметра определяет, как значение будет разобрано. Это снова та же идея фреймворка: тип — спецификация.

from fastapi import FastAPI, Path
from typing import Annotated

app = FastAPI()

@app.get("/items/{item_id}")
async def read_item(item_id: Annotated[int, Path(ge=1)]):
    return {"item_id": item_id}

Здесь item_id обязан быть целым и не меньше 1 (ge=1 — «greater or equal»). Запрос /items/5 вернёт {"item_id": 5}, а /items/abc или /items/0 — структурированную ошибку валидации. Обратите внимание на современный стиль: ограничения задаются через Annotated[int, Path(...)], а не через значение по умолчанию.

Как работает под капотом

Starlette компилирует строку маршрута в регулярное выражение, где {item_id} превращается в группу захвата. При запросе путь матчится этим регулярным выражением, захваченная подстрока — всегда текст. Затем FastAPI смотрит на аннотацию int и прогоняет текст через Pydantic-конвертацию. Смоделируем извлечение и конвертацию вручную на stdlib:

import re

# имитация компиляции "/items/{item_id}" в регулярку
pattern = re.compile(r"^/items/(?P<item_id>[^/]+)$")

def handle_path(url):
    m = pattern.match(url)
    if not m:
        return "404 Not Found"
    raw = m.group("item_id")          # всегда строка
    try:
        item_id = int(raw)            # конвертация по аннотации int
    except ValueError:
        return f"422: '{raw}' не является целым числом"
    if item_id < 1:                   # проверка ge=1
        return f"422: {item_id} меньше 1"
    return {"item_id": item_id}

for url in ["/items/5", "/items/abc", "/items/0", "/other"]:
    print(url, "->", handle_path(url))

Попробуй сам ▶ Видно три исхода: успех, ошибка типа, ошибка ограничения — ровно то, что делает FastAPI.

Важный нюанс — порядок маршрутов. Маршруты проверяются сверху вниз, поэтому фиксированный путь должен идти раньше параметризованного. Если объявить /users/{user_id} раньше /users/me, то запрос /users/me попадёт в первый и попытается превратить me в число.

Частые ошибки

Первая — ставить специфичные маршруты после общих параметризованных, из-за чего они становятся недостижимы. Вторая — забывать, что часть пути не может содержать / по умолчанию (для путей-с-слешами нужен конвертер {full_path:path}). Третья — полагаться на path-параметр там, где логичнее query: путь идентифицирует ресурс, а фильтры и опции — это query-строка.

Best practices

  • Размещайте конкретные маршруты (/users/me) выше параметризованных (/users/{id}).
  • Добавляйте ограничения через Annotated[int, Path(ge=1)] — это и валидация, и документация.
  • Используйте path для идентификации ресурса, а не для фильтров и флагов.
  • Для путей с произвольными слешами применяйте конвертер :path.

Перечисления и предопределённые значения

Иногда path-параметр должен принимать не любое значение, а одно из фиксированного набора: тип сортировки, категория, версия API. Грубое решение — принять строку и проверять её в коде, но тогда вы теряете валидацию и документацию. Правильнее объявить перечисление через Enum и указать его как тип параметра: category: ModelCategory. Тогда FastAPI сам отвергнет значения вне набора с ошибкой 422, а в документации Swagger покажет выпадающий список допустимых вариантов. Это ещё одна грань главной идеи фреймворка: чем точнее тип, тем больше работы он делает за вас. Enum в роли типа path-параметра превращает «магическую строку» в самодокументируемый контракт, который невозможно нарушить незаметно.

Итог: path-параметр объявляется фигурными скобками и одноимённым типизированным параметром; FastAPI извлекает текст из URL, конвертирует по типу и валидирует. Следите за порядком маршрутов.

Проверьте себя
1. Почему маршрут /users/me нужно объявить раньше /users/{user_id}?
AИз соображений алфавитного порядка
BИначе /users/me попадёт в параметризованный маршрут и FastAPI попытается превратить 'me' в значение типа user_id
CИначе документация не сгенерируется
DЭто не имеет значения
2. Что вернёт FastAPI на запрос /items/abc, если параметр объявлен item_id: int?
A500 Internal Server Error
BСтроку 'abc'
CОшибку валидации 422, так как 'abc' нельзя конвертировать в int
DНоль