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, типичные ошибки в схеме.