CRUD как HTTP-контракт

Отсутствующий ресурс и HTTPException

Содержание курса

get_room_or_404: общий поиск для GET /api/rooms/{room_id}

Логика «найди запись или верни 404» нужна каждый раз, когда маршрут работает с конкретным id. Чтобы не дублировать одни и те же три строки в каждом обработчике, её выносят в обычную функцию Python:

def get_room_or_404(room_id: int) -> dict:
    room = rooms_db.get(room_id)
    if room is None:
        raise HTTPException(status_code=404, detail="Room not found")
    return room

Здесь нет ничего специфичного для FastAPI: это просто вспомогательная функция. Она либо бросает исключение, либо возвращает словарь — третьего варианта нет. Обработчик GET сводится к одной строке:

@app.get("/api/rooms/{room_id}", response_model=RoomOut)
def read_room(room_id: int):
    return get_room_or_404(room_id)

Полный файл main.py с общим поиском и POST для создания записи:

from fastapi import FastAPI, HTTPException
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

def get_room_or_404(room_id: int) -> dict:
    room = rooms_db.get(room_id)
    if room is None:
        raise HTTPException(status_code=404, detail="Room not found")
    return room

@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 read_room(room_id: int):
    return get_room_or_404(room_id)

В одном запущенном процессе с пустым rooms_db создаём переговорную:

POST /api/rooms  {"name": "Mars", "capacity": 8}
→ 201  {"id": 1, "name": "Mars", "capacity": 8}

Затем читаем существующую и несуществующую:

GET /api/rooms/1
→ 200  {"id": 1, "name": "Mars", "capacity": 8}

GET /api/rooms/99
→ 404  {"detail": "Room not found"}

Важный момент: когда get_room_or_404 бросает исключение при room_id=99, строка return get_room_or_404(room_id) в read_room никогда не получает управление обратно. FastAPI перехватывает HTTPException раньше, чем обработчик успеет что-то вернуть. Поэтому response_model=RoomOut при 404 не задействуется — сериализация запускается только при успешном возврате.

Если позже понадобится маршрут PUT /api/rooms/{room_id} или любой другой, работающий с конкретной записью, он вызовет ту же get_room_or_404 и получит тот же 404 с тем же detail — без дополнительного кода.

GET /api/rooms/7 вызывает rooms_db.get(7). Найденная запись возвращается с 200. При None функция выбрасывает HTTPException, и клиент получает 404.
Для выбранного id функция возвращает запись либо выбрасывает HTTPException с 404 и detail «Room not found». None из словаря не возвращается клиенту как успешный результат.