← назад к разделу

URL — первое, что видит пользователь вашего API. Хорошо спроектированный URL понятен без документации: GET /orders/{order_id}/items читается как «получи позиции заказа». Плохой URL — это вопросы от каждого нового разработчика и ошибки при интеграции.

В этой статье разберём, как строить URL в FastAPI: какой регистр выбирать, как именовать ресурсы, какой HTTP-метод когда использовать и как не запутаться с вложенностью.

Формат пути: kebab-case и никаких слешей в конце

Представьте, что вы заходите на страницу в браузере: my-site.ru/my-page читается хорошо, my-site.ru/myPage или my-site.ru/my_page — уже вопросы. Для URL REST API действуют те же принципы.

Правила формата пути:

  • всё строчными буквами;
  • слова в сегменте разделяются дефисом: order-items, delivery-addresses;
  • без слеша в конце;
  • без расширений вроде .json — формат передаётся через заголовок Accept.
from fastapi import APIRouter

router = APIRouter(prefix="/api/v1", redirect_slashes=False)

@router.get("/order-items")
async def get_order_items(): ...

@router.get("/delivery-addresses")
async def get_delivery_addresses(): ...

Частые ошибки:

/api/v1/OrderItems        # заглавные буквы — нет
/api/v1/order_items       # нижнее подчёркивание — нет
/api/v1/deliveryAddresses # camelCase — нет
/api/v1/orders/           # слеш в конце — нет
/api/v1/orders.json       # расширение в пути — нет

Почему redirect_slashes=False обязателен

По умолчанию FastAPI настроен так, что запрос на /orders/ перенаправляется на /orders кодом 307 Temporary Redirect. Это маскирует ошибку: клиент думал, что правильный URL — со слешем, и молча получал редирект. Лучше сразу возвращать ошибку, чтобы разработчик исправил URL.

router = APIRouter(prefix="/api/v1", redirect_slashes=False)

Служебные эндпоинты вне версии

/health, /ready, /metrics не относятся к бизнес-API, поэтому живут вне /api/v1:

from fastapi import FastAPI

app = FastAPI()

@app.get("/health")
async def health(): return {"status": "ok"}

@app.get("/ready")
async def ready(): return {"status": "ok"}

Их не версионируют и не закрывают аутентификацией — они для инфраструктуры.

Как именовать ресурсы

REST строится вокруг ресурсов — сущностей, с которыми работает API. Ресурс — это заказ, пользователь, товар. URL должен называть ресурс, а не действие над ним.

Коллекции — во множественном числе:

@router.get("/orders")               # список заказов
@router.get("/orders/{order_id}")    # один заказ
@router.get("/products")             # список товаров

Singleton-ресурс (всегда один на контекст) — в единственном:

@router.get("/users/{user_id}/profile")   # профиль пользователя — он один

Имена берут из домена: если в коде и документации сущность называется Order, то и в URL — /orders. Не /purchases, не /transactions — только то слово, которое используется в проекте.

Order    → /orders
Product  → /products
Customer → /customers

Когда имена расходятся в коде, документации и URL — каждый разработчик в команде понимает API по-своему. Единство терминологии снижает число вопросов.

Какой HTTP-метод выбрать

В REST метод запроса несёт смысл: он говорит, что вы хотите сделать с ресурсом. В FastAPI метод задаётся декоратором.

@router.get("/orders", status_code=200)
async def get_orders(): ...                        # читаем список

@router.post("/orders", status_code=201)
async def create_order(): ...                      # создаём заказ

@router.put("/orders/{order_id}", status_code=200)
async def replace_order(order_id: str): ...        # полная замена

@router.patch("/orders/{order_id}", status_code=200)
async def update_order(order_id: str): ...         # частичное обновление

@router.delete("/orders/{order_id}", status_code=204)
async def delete_order(order_id: str): ...         # удаляем
ДекораторЧто делаетКод успеха
@router.getчитает ресурс200
@router.postсоздаёт ресурс или выполняет команду201
@router.putполностью заменяет ресурс200
@router.patchчастично обновляет ресурс200
@router.deleteудаляет ресурс204

Частая ошибка: использовать GET для операций с побочным эффектом. GET — только для чтения, он не должен ничего менять.

# Правильно: команда с побочным эффектом — POST
@router.post("/orders/{order_id}/cancel")
async def cancel_order(order_id: str): ...

# Неправильно: GET изменяет состояние
@router.get("/orders/{order_id}/cancel")
async def cancel_order(order_id: str): ...

Вложенность ресурсов

Иногда один ресурс логически принадлежит другому: позиции заказа существуют в контексте заказа. В таких случаях используют вложенные URL.

@router.get("/orders/{order_id}/items")
async def get_order_items(order_id: str): ...

@router.get("/orders/{order_id}/items/{item_id}")
async def get_order_item(order_id: str, item_id: str): ...

Два уровня вложенности — нормально. Три и больше — уже сложно читать и поддерживать.

Если нужно выйти за два уровня, лучше перейти к плоскому ресурсу с фильтром:

# Три уровня — сложно
# /users/{user_id}/orders/{order_id}/items/{item_id}

# Лучше: плоский ресурс с параметром
@router.get("/items")
async def get_items(order_id: str = Query(alias="orderId")): ...

Имена параметров пути

При нескольких вложенных ресурсах параметры пути должны иметь уникальные имена, иначе Swagger UI и Redoc не смогут правильно отобразить документацию.

# Правильно: уникальные имена
@router.get("/orders/{order_id}/items/{item_id}")
async def get_order_item(order_id: str, item_id: str): ...

# Неправильно: одинаковые имена
@router.get("/orders/{id}/items/{id}")   # ошибка в инструментах документации

Коротко

  • Путь — строчными буквами, слова через дефис (order-items), без слеша в конце.
  • redirect_slashes=False обязателен, иначе FastAPI молча редиректит ошибочные URL.
  • Коллекции — во множественном числе (/orders), singleton — в единственном (/profile).
  • Имена ресурсов берут из домена: одно слово — везде.
  • Метод задаётся декоратором: @router.get — чтение, @router.post — создание или команда.
  • GET не должен изменять состояние — для команд с побочным эффектом используют POST.
  • Вложенность — не глубже двух уровней; глубже — плоский ресурс с фильтром.
  • Параметры пути при нескольких уровнях должны иметь уникальные имена.

Что почитать дальше

  • Версионирование REST API на FastAPI — как использовать prefix="/api/v1" и переходить на v2.
  • Query-параметры и пагинация — фильтры, Query(alias=...), курсорная пагинация.
  • OpenAPI и антипаттерны — operation_id, tags, типичные ошибки в схеме.