Learning Platform
Глоссарий Troubleshooting
Урок 06.03 · 25 мин
Начальный
url-designversioningnamingquery-parametersuri

URL design и versioning: как назвать ресурсы и пережить breaking changes

URL API — это публичный контракт. Один раз опубликовал /v1/users — будешь поддерживать пять лет. Поменяешь URL — сломаешь всех клиентов, кто на тебя интегрирован. Это не “как назвать функцию” в коде — это куда дороже. И именно поэтому большинство известных API застряли с уродливыми именами образца 2010 года: меняться нельзя.

В этом уроке разберёмся, как проектировать URL для REST API так, чтобы было читаемо, предсказуемо, масштабируемо, и какие четыре стратегии версионирования существуют. Каждая стратегия имеет цену — кто-то ломается реже, кто-то требует лучшей инфраструктуры. Junior DE должен понимать tradeoff-ы, чтобы не выстрелить себе в ногу первым же дизайн-решением.


Главное правило: nouns, not verbs

REST оперирует ресурсами (вспомним урок 1). URL — это identifier ресурса, не имя функции. Соответственно, URL — это существительные.

Плохо (RPC-стиль)Хорошо (REST-стиль)
POST /createUserPOST /users
GET /getUserById?id=123GET /users/123
POST /deleteUser?id=123DELETE /users/123
POST /user/updatePUT /users/123 или PATCH /users/123
GET /listAllOrdersGET /orders

Глагол кодируется в HTTP методе (GET/POST/PUT/DELETE), а не в URL. Это и есть uniform interface.

Исключение: action-style операции, которые не вписываются в CRUD. Как назвать “перезагрузить сервер”? “Заархивировать заказ”? Один из паттернов:

POST /orders/123/archive          # action как sub-resource
POST /orders/123:archive          # gRPC-style двоеточие (Google API)
POST /orders/123/actions/archive  # явный action namespace

Худший вариант — /archiveOrder?id=123. Лучший — POST /orders/123/archive. Это всё ещё nouns (есть ресурс “archive у заказа”), но допустимое отклонение.

NOTE

Google API Design Guide использует двоеточие для custom actions: POST /v1/users/123:resetPassword. Это формально позволено в URL по RFC 3986, но странновато выглядит. Большинство индустрии использует sub-resource подход. Junior DE должен уметь читать оба варианта.


Иерархия: родитель-ребёнок

Если у тебя есть владение между ресурсами — отрази его в URL.

/users/{user_id}/orders                    # все заказы конкретного пользователя
/users/{user_id}/orders/{order_id}         # конкретный заказ конкретного пользователя
/users/{user_id}/orders/{order_id}/items   # позиции в заказе

Зачем:

  • Понятно из URL, кто чей.
  • Легко делать access control: пользователь Alice может видеть только /users/alice/....
  • Натурально для иерархических данных.

Когда НЕ стоит:

  • Если ресурс может принадлежать разным владельцам — лучше плоский URL /orders/{order_id} с фильтром ?user_id={user_id}.
  • Если URL получается длиннее 4-5 уровней — это запах дизайна. /users/123/orders/456/items/789/discount/details — это ад. Делай плоский ресурс.
Иерархия vs flat: когда какой подход
Иерархия: ресурс ВСЕГДА принадлежит конкретному родителю
/users/123/orders/456/users/123/orders/456 -- заказ всегда принадлежит пользователю
+ явное владениеПлюс: явное владение, упрощает RBAC
- длинноМинус: длинные URL при глубокой иерархии
Flat: ресурс самодостаточен, фильтры через query
/orders/456/orders/456?user_id=123 -- заказ как самостоятельная сущность
+ краткоПлюс: короткие URL, простая выборка
- нужны фильтрыМинус: нужен фильтр в query или ID-prefix чтобы выбирать чужие

Plural или singular?

Соглашение в индустрии: plural для коллекций и одиночных элементов в коллекции. Singular для уникальных ресурсов без множественной формы.

/users          # коллекция -- plural
/users/123      # один пользователь из коллекции -- всё ещё plural
/me             # текущий пользователь, один -- singular
/profile        # профиль текущего пользователя, один -- singular
/account        # аккаунт текущего пользователя, один -- singular

Почему plural: консистентность. URL /users/123 читается как “пользователь номер 123 из коллекции users”. URL /user/123 создаёт впечатление, что есть много “user” сущностей.

Не смешивай: в одном API не должно быть /users и /order (один plural, другой singular). Будь последователен.


Query parameters: фильтры, сортировка, пагинация

Query strings (?key=value&key2=value2) — для необязательных параметров запроса: фильтры, сортировка, пагинация, выбор полей.

# Фильтрация
GET /users?role=admin
GET /users?status=active&country=RU
GET /orders?created_after=2026-01-01&status=paid

# Сортировка (соглашение: minus для desc)
GET /users?sort=name           # по имени asc
GET /users?sort=-created_at    # по created_at desc
GET /users?sort=country,-age   # по country asc, потом age desc

# Пагинация (offset/limit)
GET /users?offset=20&limit=10
# Или page-based
GET /users?page=2&per_page=10
# Или cursor-based (для больших коллекций -- идемпотентнее)
GET /users?cursor=eyJpZCI6MTAwfQ&limit=20

# Sparse fieldsets (выбор подмножества полей -- экономит трафик)
GET /users?fields=id,name,email
TIP

Query parameters не должны быть обязательными для идентификации ресурса. /users?id=123 — плохо: id это идентификатор, должен быть в path -> /users/123. Query — это про “как выбрать/отфильтровать/отсортировать”, не про “что выбрать”.


Versioning: четыре стратегии

Режимы совместимости схем в Kafka Релиз-цикл, API versioning, deprecation в Kubernetes

Рано или поздно тебе придётся сделать breaking change в API: переименовать поле, поменять тип, удалить endpoint. Чтобы не ломать клиентов одномоментно — нужен versioning. Четыре стратегии в индустрии.

Стратегия 1: URL path versioning

GET /v1/users/123
GET /v2/users/123

Плюсы:

  • Очевидно из URL, какая версия.
  • Тривиально маршрутизировать на разные backend-ы (/v1/* -> старый сервис, /v2/* -> новый).
  • Кэшируется чисто (разные URL — разные cache entries).

Минусы:

  • URL непостоянны при upgrade — клиент должен переписать все references при переходе.
  • Технически нарушает REST: один resource не должен иметь два URL (но в индустрии это закрывают глаза).

Кто использует: Stripe (/v1/...), GitHub (/v3/... через subdomain), Twilio. Самая популярная стратегия в индустрии.

Стратегия 2: Query parameter versioning

GET /users/123?version=1
GET /users/123?version=2
GET /users/123?api-version=2026-05-01

Плюсы:

  • URL ресурса стабилен.
  • Удобно для постепенной миграции — можно дефолтить если параметр не указан.

Минусы:

  • Query parameter может быть забыт — поведение зависит от дефолта (часто меняется).
  • Грязнее в кэшировании (нужно кэшировать с учётом query).

Кто использует: Azure (api-version=2024-08-01). Дата вместо версии — ещё один паттерн (rolling versioning).

Стратегия 3: Accept header versioning (media type)

GET /users/123
Accept: application/vnd.example.v1+json

GET /users/123
Accept: application/vnd.example.v2+json

Плюсы:

  • “Чистый” REST: один resource, один URL, разные representations.
  • Версия — это часть representation, что концептуально правильно.

Минусы:

  • Тяжелее тестировать в браузере (надо ставить header, а в адресной строке этого не сделаешь).
  • Сложнее для junior разработчиков и debugging.

Кто использует: GitHub (Accept: application/vnd.github.v3+json), Salesforce. Второй по популярности подход.

Стратегия 4: Custom header

GET /users/123
X-API-Version: 2

Плюсы:

  • URL чистый, версия отдельно.
  • Легко изменить через middleware.

Минусы:

  • Не стандартизировано — каждый API называет header по-своему.
  • Похоже на Accept-стратегию, но без официального RFC backing.

Кто использует: небольшие API. Не самая распространённая.


Сравнительная таблица

СтратегияПримерURL стабиленCache-friendly”Чистый” RESTBrowser-friendly
URL path/v1/usersнетданетда
Query param/users?v=1дасреднесреднеда
Accept headerAccept: ...v1+jsonданужен Varyданет
Custom headerX-API-Version: 1данужен Varyсредненет
WARNING

Для header-based versioning сервер ОБЯЗАН выставлять Vary: Accept (или Vary: X-API-Version) в ответе. Иначе CDN/proxy закэширует ответ для одной версии и отдаст его клиентам с другой версией. Это распространённый источник багов.


Когда вообще нужно версионировать

Не каждое изменение требует новой версии. Различай:

Backward-compatible (без новой версии):

  • Добавление новых полей в response — старые клиенты их игнорируют.
  • Добавление новых необязательных параметров запроса.
  • Добавление новых endpoint-ов.
  • Расширение enum-а (но осторожно — клиенты часто плохо обрабатывают неизвестные значения).

Breaking changes (нужна новая версия):

  • Удаление или переименование поля.
  • Изменение типа поля (int -> string).
  • Изменение формата дат.
  • Удаление endpoint-а или метода.
  • Изменение семантики статус-кода (200 -> 204).
  • Сужение enum-а.

Стратегия “не версионировать вообще, только добавлять” работает на удивление долго. Stripe держал /v1/ 14 лет (с 2011) и не выпустил /v2/. Они только меняют поведение по Stripe-Version header (rolling versions).


Попробуй сам: проанализируй URL у популярных API

import requests

# GitHub: URL path + Accept header
r = requests.get(
    "https://api.github.com/users/torvalds",
    headers={"Accept": "application/vnd.github.v3+json"}
)
print(r.url)  # version в Accept header

# Stripe: URL path
r = requests.get(
    "https://api.stripe.com/v1/charges",
    auth=("sk_test_...", "")
)
print(r.url)  # https://api.stripe.com/v1/charges

# Сравни структуру URL у GitHub
# /repos/{owner}/{repo}/issues       -- иерархия repos -> issues
# /repos/{owner}/{repo}/issues/{n}   -- конкретный issue
# /search/repositories?q=...         -- search как отдельный resource

# Twilio
# /2010-04-01/Accounts/{sid}/Messages -- date-based versioning в URL

Открой документацию двух разных API и составь таблицу: какая стратегия URL, какая versioning, есть ли иерархия. Учись видеть design decisions.


DE-контекст: типичные ошибки

  1. Hardcoded URL versions: BASE_URL = "https://api.x.com/v1" в коде — поломаешься при переходе на v2. Лучше вынести API_VERSION в конфиг.
  2. Игнорирование deprecation headers: API часто шлёт Deprecation: true или Sunset: Fri, 31 Dec 2026 23:59:59 GMT. Логируй такие предупреждения, иначе пропустишь окно миграции.
  3. Pagination в Airflow tasks: offset-пагинация может терять/дублировать данные если коллекция меняется во время скачивания. Cursor-based — стабильнее, всегда выбирай его если API даёт.
  4. Query string в логах: не логируй полный URL если в query есть api_key=.... Маскируй или используй headers для секретов.

Killer takeaway

URL это nouns, не verbs — глагол в HTTP методе. Используй plural (/users) для коллекций, singular (/me) для уникальных ресурсов. Иерархия в URL отражает владение, но не глубже 3-4 уровней. Query parameters — для фильтров/сортировки/пагинации, не для идентификации ресурса. Versioning: четыре стратегии. URL path (/v1/) — самая популярная и удобная для cache. Accept header (vnd.x.v1+json) — самая “RESTful”. Query (?v=1) и custom header — нишевые. Не версионируй для backward-compatible изменений (новые поля, новые endpoint-ы). Версионируй для breaking (удаление, переименование, смена типа).

Проверка знанийKnowledge check
ОтветAnswer

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 5. Какой из URL соответствует REST-стилю?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 4