Урок курса

Path-параметры и типы

FastAPI для начинающих: API с базой данных и тестами

В прошлом уроке мы зарегистрировали маршрут GET /api/rooms и получили список комнат одним декоратором. Теперь добавим второй маршрут, который принимает конкретный идентификатор прямо в URL.

Значение из пути и аннотация int

Когда нужно обратиться к конкретному ресурсу — не ко всему списку, а к одной комнате — принято помещать идентификатор прямо в путь: /api/rooms/3. Чтобы FastAPI понял, что 3 — это переменная, а не фиксированный текст, соответствующий сегмент пути оборачивают в фигурные скобки.

@app.get("/api/rooms/{room_id}")
def get_room(room_id):
    return {"room_id": room_id}

Имя внутри скобок — room_id — должно совпадать с именем аргумента функции. Для GET /api/rooms/3 FastAPI извлечёт строку "3" из URL и передаст её в room_id. Именно строку: без аннотации типа никакого преобразования нет, и обработчик вернёт {"room_id": "3"}.

Добавим аннотацию:

@app.get("/api/rooms/{room_id}")
def get_room(room_id: int):
    return {"room_id": room_id}

Теперь тот же запрос GET /api/rooms/3 вернёт {"room_id": 3} — уже число. FastAPI прочитал строку "3" из пути, преобразовал её в int и только затем вызвал функцию. Сам Python этого не делает: обычная аннотация типа не превращает аргумент автоматически. Преобразование — работа FastAPI, выполняется до входа в тело функции.

Неверный тип: ответ 422 до обработчика

Раз преобразование происходит до вызова функции, FastAPI может отклонить запрос ещё на входе — если значение не удаётся привести к нужному типу.

Отправьте GET /api/rooms/abc к маршруту с room_id: int. Строка "abc" не превращается в целое число, и функция get_room вообще не запускается. В ответ придёт статус 422 Unprocessable Entity и JSON с описанием проблемы:

{
  "detail": [
    {
      "type": "int_parsing",
      "loc": ["path", "room_id"],
      "msg": "Input should be a valid integer, unable to parse string as an integer",
      "input": "abc"
    }
  ]
}

Поле loc показывает источник: "path" — значит, ошибка в сегменте пути, "room_id" — конкретный параметр. Это удобно, когда параметров несколько и нужно быстро найти виновника.

422 отличается от ошибки, которая возникла бы внутри функции. Здесь тело обработчика не выполнялось вовсе: FastAPI остановил запрос до него. Это граница между некорректным входом и логикой приложения.

При room_id: int FastAPI преобразует строку 3 из пути в число 3 и вызывает get_room(3). Строка abc не проходит преобразование: возвращается 422, get_room не вызывается.
FastAPI проверяет и преобразует значение до вызова обработчика. Неверный тип даёт 422 без выполнения функции.

Поиск по id: существующий ключ и KeyError

Допустим, у нас есть каталог из двух переговорных комнат, хранящийся в словаре с целочисленными ключами:

from fastapi import FastAPI

app = FastAPI()

rooms_by_id = {
    1: {"id": 1, "name": "Лондон", "floor": 2},
    2: {"id": 2, "name": "Берлин", "floor": 3},
}

@app.get("/api/rooms")
def list_rooms():
    return list(rooms_by_id.values())

@app.get("/api/rooms/{room_id}")
def get_room(room_id: int):
    return rooms_by_id[room_id]

GET /api/rooms/1 вернёт {"id": 1, "name": "Лондон", "floor": 2}. FastAPI преобразует "1" в 1, функция обращается к словарю по ключу 1 — всё работает.

GET /api/rooms/abc — 422 ещё до словаря, как разобрали выше.

GET /api/rooms/999 — интереснее. Строка "999" успешно преобразуется в int, проверка типа пройдена. Но ключа 999 в rooms_by_id нет. Python выбросит KeyError, FastAPI не поймает его автоматически, и клиент получит 500 Internal Server Error.

Вот принципиальное различие: аннотация int гарантирует только форму значения — что оно целое число. Она ничего не знает о содержимом словаря. Проверка типа и проверка существования записи — два независимых шага. Первый делает FastAPI, второй — ваш код.

Попробуйте решить

Маршрут объявлен как @app.get("/api/rooms/{room_id}") с функцией def get_room(room_id: int). Клиент отправляет запрос GET /api/rooms/5. Какое значение и какого типа получит функция-обработчик в аргументе room_id?

Продолжить с проверкой и прогрессом

Откройте интерактивный раннер с заданиями урока.

Перейти к интерактивному уроку