Topic 5 of 6

Course materials are currently available in Russian only.

Ответы и ошибки

Хорошее API отвечает предсказуемо: правильным кодом, понятным телом ошибки и нужными заголовками.

Коды ответов

Код Когда
200 OK обычный успешный ответ
201 Created создана запись
204 No Content успешно, тела нет (часто на DELETE)
400 Bad Request неверный запрос
401 / 403 не представились / нельзя
404 Not Found записи нет
409 Conflict запрос противоречит данным (такая запись уже есть)
422 данные не прошли проверку (FastAPI отвечает сам)

Вместо чисел можно писать константы: from fastapi import status → status.HTTP_409_CONFLICT.

Свои исключения

Когда ошибок много, удобно завести своё исключение и одно место, которое превращает его в ответ:

from fastapi import Request
from fastapi.responses import JSONResponse


class AppError(Exception):
    def __init__(self, code: int, message: str):
        super().__init__(message)
        self.code = code
        self.message = message


@app.exception_handler(AppError)
def app_error(request: Request, exc: AppError):
    return JSONResponse(status_code=exc.code, content={"error": exc.message})


@app.post("/todos/{tid}/done")
def mark_done(tid: int):
    if tid not in TODOS:
        raise AppError(404, "Задача не найдена")
    ...

Код логики не думает о формате ответа — он просто бросает исключение. А если формат ошибок понадобится поменять, правка будет в одном месте.

Заголовки и пустые ответы

from fastapi import Response


@app.post("/todos", status_code=201)
def create(todo: TodoIn, response: Response):
    ...
    response.headers["Location"] = f"/todos/{item['id']}"
    return item


@app.delete("/todos/{tid}", status_code=204)
def delete(tid: int):
    ...
    return Response(status_code=204)
  • параметр с типом Response — объект будущего ответа: в него пишут заголовки (response.headers), cookie (response.set_cookie) и код (response.status_code);
  • заголовок Location в ответе 201 сообщает адрес созданной записи;
  • для 204 тела быть не должно, поэтому возвращают пустой Response.

Номера записей

Номер новой записи нельзя считать как len(TODOS) + 1: после удаления номера начнут повторяться, и новая задача затрёт старую. Используйте счётчик, который только растёт, — itertools.count(1) и next(ids). В базе данных этим занимается сама база.

Итог

  • коды 201, 204, 409 и константы status.HTTP_…;
  • своё исключение + @app.exception_handler + JSONResponse;
  • response: Response — заголовки, Location для созданной записи;
  • Response(status_code=204); номера — растущим счётчиком.