Урок курса

Создание ресурса: POST и статус 201

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

В предыдущем уроке RoomIn описывала входные данные, а RoomOut отбирала публичные поля ответа. Обработчик возвращал демонстрационную карточку без сохранения. Чтобы созданную переговорную можно было затем прочитать через GET, обработчикам нужно общее хранилище.

status_code=201: статус успешного создания

HTTP разделяет два разных успеха: 200 означает «запрос выполнен», а 201 — «запрос выполнен и ресурс создан». Если POST-обработчик просто возвращает данные без явного статуса, FastAPI отвечает 200. В нашем API создание переговорной должно возвращать 201: ответ 200 не соответствует этому выбранному контракту. Сам по себе статус 200 у POST не является нарушением HTTP — POST может выполнять и другие действия.

Чтобы изменить статус, в декоратор добавляют параметр status_code:

@app.post("/api/rooms", response_model=RoomOut, status_code=201)
def create_room(room: RoomIn):
    ...

Теперь при успешном завершении функции FastAPI отправит статус 201, а не 200.

Важно понимать, что status_code=201 — это только инструкция для ответа. Он говорит клиенту, что ресурс создан, но сам по себе не сохраняет никаких данных. Если тело функции не добавит запись в какое-то хранилище, ответ придёт с кодом 201, но при следующем GET записи не окажется. Статус декларирует намерение; реализацию хранения берёт на себя обработчик.

rooms_db и next_id: сохранение переговорной обработчиком POST

Для учебного примера хранилище — обычный Python-словарь на уровне модуля. Рядом с ним объявляют счётчик:

rooms_db: dict[int, dict] = {}
next_id = 1

Оба имени живут в глобальной области модуля. Это значит, что все обработчики в том же файле работают с одним и тем же объектом — не с копиями.

Вот полный файл main.py с POST и GET:

from fastapi import FastAPI
from pydantic import BaseModel, Field

app = FastAPI()

rooms_db: dict[int, dict] = {}
next_id = 1

class RoomIn(BaseModel):
    name: str = Field(..., min_length=2, max_length=50)
    capacity: int = Field(..., ge=1, le=50)

class RoomOut(BaseModel):
    id: int
    name: str
    capacity: int

@app.post("/api/rooms", response_model=RoomOut, status_code=201)
def create_room(room: RoomIn):
    global next_id
    room_id = next_id
    rooms_db[room_id] = {"id": room_id, "name": room.name, "capacity": room.capacity}
    next_id += 1
    return rooms_db[room_id]

@app.get("/api/rooms/{room_id}", response_model=RoomOut)
def get_room(room_id: int):
    return rooms_db[room_id]

Что происходит внутри create_room при каждом вызове: функция сначала читает текущее значение next_id и сохраняет его в локальную переменную room_id. Затем записывает словарь с тремя полями в rooms_db под этим ключом. После этого увеличивает счётчик, чтобы следующий вызов получил уже другой id. Наконец, возвращает только что сохранённую запись — FastAPI пропустит её через RoomOut и отправит клиенту.

global next_id нужен именно здесь: без этого объявления Python считает присвоение next_id += 1 созданием новой локальной переменной и поднимает UnboundLocalError.

Если отправить два POST подряд в одном запущенном процессе, первый создаст запись с id=1, второй — с id=2. Обе записи останутся в rooms_db до завершения процесса. При перезапуске сервера словарь снова пустой, а счётчик снова равен 1 — никакой персистентности нет. Эта схема работает для последовательных запросов к одному процессу; при параллельных вызовах или нескольких воркерах согласованность id не гарантируется.

GET по id из POST: проверка сохранённой записи

Статус 201 в ответе POST сообщает, что ресурс создан. Но чтобы убедиться, что запись действительно доступна через API, нужно её прочитать — отдельным GET-запросом.

Алгоритм простой: берёте id из тела ответа POST и делаете GET /api/rooms/{id} в том же запущенном процессе.

Например, после POST с телом {"name": "Берлин", "capacity": 8} сервер вернёт:

{"id": 1, "name": "Берлин", "capacity": 8}

Статус — 201. Теперь запрашиваете GET /api/rooms/1 и получаете:

{"id": 1, "name": "Берлин", "capacity": 8}

Статус — 200. Тело совпадает с тем, что вернул POST.

Что делает get_room в коде: он просто читает rooms_db[room_id] и возвращает запись. FastAPI снова применяет RoomOut и отправляет клиенту отфильтрованный ответ.

Если запросить id, которого нет в словаре — например, GET /api/rooms/99 после единственного POST — Python поднимет KeyError, и FastAPI вернёт 500. Обработка отсутствующей записи через HTTPException — отдельная тема следующего урока. Сейчас проверяйте только id из реального ответа POST, не выходя за пределы уже созданных записей.

Данные не переживают перезапуск сервера: после остановки процесса rooms_db очищается, и тот же GET /api/rooms/1 снова вернёт ошибку.

POST /api/rooms с названием Берлин и capacity 8 сохраняет запись rooms_db[1] и возвращает 201. GET /api/rooms/1 читает ту же запись и возвращает 200 с теми же данными.
POST сохраняет «Берлин», 8 мест, и возвращает 201. Последующий GET по полученному id подтверждает доступность той же записи ответом 200 в этом процессе.

Задача 1

Обработчик POST возвращает корректный словарь, соответствующий RoomOut. В декораторе @app.post("/api/rooms", response_model=RoomOut) параметр status_code не указан. Какой статус FastAPI использует для этого успешного ответа?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 2

Клиент отправил POST /api/rooms с корректным телом и получил ответ со статусом 201 и телом {"id": 3, "name": "Прага", "capacity": 6}. Затем клиент отправил GET /api/rooms/3. Сервер использует тот же rooms_db, в который POST записал данные.

Какой статус и тело ожидает клиент от GET-запроса, если запись с id=3 присутствует в rooms_db?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 3

Сервер запущен с маршрутами из примера урока (POST /api/rooms с status_code=201, GET /api/rooms/{room_id}). Хранилище пустое.

Клиент выполняет три операции:

  1. POST /api/rooms с {"name": "Варшава", "capacity": 10}
  2. POST /api/rooms с {"name": "Рига", "capacity": 5}
  3. GET /api/rooms/2

Какой HTTP-статус вернёт третий запрос (GET /api/rooms/2) и какое значение поля name будет в теле ответа? Ответ: статус и name через запятую.

Без регистрации · результат не сохраняется

Задача 4

Переговорные остаются в каталоге

Офис-менеджер ведёт каталог переговорных в сервисе Roomly: каждая комната со своим описанием должна занять в нём отдельное, чётко различимое место. Когда менеджер передаёт сведения об очередной переговорной, сервис принимает их и возвращает идентификатор — уникальную метку, по которой именно эту запись можно будет найти снова.

Ключевое требование состоит в том, что новая запись не должна вытеснять уже существующие: добавление второй и третьей комнаты лишь пополняет каталог, но никак не затрагивает то, что было внесено раньше. Идентификатор, однажды выданный для конкретной переговорной, остаётся верным адресом этой записи на всё время текущей работы с сервисом.

Менеджеру недостаточно просто получить подтверждение в момент добавления — он хочет убедиться, что сведения каждой комнаты действительно доступны и после того, как в каталог были внесены следующие. Именно эту уверенность и должна давать программа: последовательно добавленные переговорные остаются в каталоге, и каждую из них можно запросить по её идентификатору, не опасаясь, что более поздние записи её перекрыли.

Что уже дано

Это самостоятельный файл main.py для браузерного редактора: код ваших прежних решений не переносится автоматически. Модели Address, RoomIn, RoomOut, верхнеуровневый app, пустой rooms_db и начальный next_id уже в файле. Это отдельное упражнение на полную карточку из урока11, а не объединение всех прежних каталогов и фильтров. GET уже читает существующую запись. Демонстрационный POST пока только возвращает карточку без сохранения; доработайте его.

Служебный переходник выполняется перед файлом, но не создаёт app или данные. Он только передаёт запросы и результаты между приложением и скрытой проверкой; менять его не нужно.

Что нужно сделать

Доработайте POST /api/rooms: корректное описание создаёт отдельную запись, получает новый серверный целочисленный id и ответ201. GET по выданному id возвращает200 с той же полной карточкой. Несколько последовательных POST сохраняют все прежние записи, в том числе когда описания двух комнат совпадают: это два добавления, а не замена. Имена обработчиков выбирайте сами.

Готовая RoomIn принимает обязательные name (строка длиной2–50), capacity (целое1–50) и address (объект с обязательными строками city, street). comment — необязательная строка или null, default — None. Сохраните эту валидацию: неверное тело получает стандартный422, включая путь вложенного поля. Публичная карточка содержит ровно id, name, capacity, address, comment; internal_note наружу не попадает. Лишние входные поля игнорируются; клиентские id и internal_note не подменяют серверные данные.

Проверяются последовательные запросы в одном запуске. Сохранение после перезапуска не требуется. В этом уроке GET вызывается только для уже созданных id; обработка отсутствующей записи, PUT/PATCH/DELETE не нужны.

Ввод и вывод

stdin не используется. Печатать через print в stdout ничего не нужно. После выполнения файла проверяющая система обращается к верхнеуровневому app: публичный переходник вызывает TestClient внутри процесса приложения и передаёт результат скрытой проверке. Запускать Uvicorn, сеть, клиентский скрипт или pytest не требуется.

POST /api/rooms с {"name":"Липа","capacity":4,"address":{"city":"Омск","street":"Мира"}} создаёт карточку и возвращает201. Если получен id=1, GET /api/rooms/1 вернёт200 и {"id":1,"name":"Липа","capacity":4,"address":{"city":"Омск","street":"Мира"},"comment":null}.

РешениеPython

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задачи к уроку

Все задачи курса →

Продолжить с сохранением прогресса

Перейдите к уроку в учебном кабинете.

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