Урок курса
Создание ресурса: 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 с теми же данными.](/uploads/lesson-media/post_store_get-1788127244128-758fc25a-571b-4d26-8aba-95230d3acadb.png)
Попробуйте решить
Обработчик POST возвращает корректный словарь, соответствующий RoomOut. В декораторе @app.post("/api/rooms", response_model=RoomOut) параметр status_code не указан. Какой статус FastAPI использует для этого успешного ответа?
Продолжить с проверкой и прогрессом
Откройте интерактивный раннер с заданиями урока.
