Создание ресурса в FastAPI: статус 201 и заголовок Location

POST вернул «готово», но клиенту всё равно приходится угадывать адрес новой записи. Полезный ответ создания содержит идентификатор, данные и адрес ресурса. Статус 201 сообщает о завершённом создании, а Location даёт точку следующего обращения.

Адрес должен строиться по маршруту

Не склеивайте hostname и путь вручную в каждом обработчике. request.url_for использует имя маршрута и его path-параметры. Если вы перенесёте API под другой префикс, связанные имена помогут сохранить правильный адрес. Имя должно быть уникальным и осмысленным, чтобы ссылка указывала на нужную операцию.

201 подходит после фактического создания. Когда команда только принята в очередь, клиенту обычно нужен 202 и адрес задачи. В примере запись уже находится в хранилище к моменту ответа, поэтому GET по Location сразу возвращает тот же ресурс. В базе этот порядок должен включать успешный commit.

Проверим переход по выданной ссылке

Для запуска нужны FastAPI и HTTPX. Response позволяет добавить заголовок, сохранив обычную сериализацию словаря. Тест берёт Location из реального ответа и вызывает его, не повторяя формулу построения URL в проверке.

Сохраните весь блок в файл и запустите Python 3.11+ в отдельном окружении с пакетами fastapi, httpx. Для моделей используется Pydantic v2; отдельный сервер для TestClient не нужен.

from fastapi import FastAPI, Request, Response, HTTPException
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field

app = FastAPI()
items = {}
class ItemIn(BaseModel):
    name: str = Field(min_length=1)

@app.get("/api/items/{item_id}", name="read_item")
def read_item(item_id: int):
    if item_id not in items:
        raise HTTPException(404, "item not found")
    return items[item_id]

@app.post("/api/items", status_code=201)
def create_item(data: ItemIn, request: Request, response: Response):
    item_id = len(items) + 1
    item = {"id": item_id, "name": data.name}
    items[item_id] = item
    response.headers["Location"] = str(request.url_for("read_item", item_id=item_id))
    return item

with TestClient(app) as client:
    created = client.post("/api/items", json={"name": "Lamp"})
    assert created.status_code == 201
    assert created.json() == {"id": 1, "name": "Lamp"}
    location = created.headers["location"]
    assert location == "http://testserver/api/items/1"
    assert client.get(location).json() == created.json()
print("created resource is readable through Location")

Что проверить в опубликованном приложении

За прокси url_for опирается на схему и host запроса, известные приложению. Если Location содержит внутренний адрес, проверьте конфигурацию прокси и доверенных forwarded-заголовков. Локальный тест подтверждает путь приложения, а внешний проверяет действительный публичный origin. Не подставляйте непроверенный клиентский заголовок в ссылку вручную.

Если frontend на другом origin читает Location из JavaScript, разрешите этот заголовок через CORS expose_headers. Сам факт его наличия в HTTP-ответе не всегда делает его доступным браузерному коду. Для обычного backend-клиента такого ограничения чтения нет.

Как сохранить договор при развитии API

Новый id должен оставаться идентификатором ресурса, а не номером операции в памяти. Для постоянной базы используйте сгенерированное значение после flush или commit и убедитесь, что ссылка содержит именно его. Не отдавайте клиенту заранее вычисленный id, если вставка ещё способна завершиться отказом.

Проверка перехода по Location связывает создание и чтение. Она обнаружит переименование пути, неверный параметр и ссылку на отсутствующую запись. Добавьте отдельный тест неуспешного создания: при 422 или 409 заголовок нового ресурса не должен создавать впечатление успешной операции.

Следующая практика

Открытый урок создания ресурса посвящён POST и статусу 201. Выполните базовый сценарий, затем добавьте Location и проверку чтения по выданному адресу, чтобы связать создание с дальнейшей работой клиента. Перейдите к бесплатной практике POST и 201.

Бесплатный курс FastAPI поможет связать создание ресурса с базой, авторизацией и тестами. После сохранения записи проверяйте не только полученный id, но и следующую операцию клиента.

Продолжить на практике