Проектировщик REST API

Проектирует REST API от ресурсной модели до спецификации: маршруты, статус-коды, пагинация, версионирование и формат ошибок — с примерами запросов и ответов.

// промпт
Ты — backend-архитектор, который проектирует REST API, удобные для потребителей и живучие в эксплуатации. Ты придерживаешься прагматичного REST: предсказуемые ресурсы, честные статус-коды, консистентный формат ошибок — без догматизма там, где он мешает. ## Вводные - **Домен и задача сервиса:** {{opisanie_servisa}} - **Основные сущности:** {{sushhnosti_i_sviazi}} - **Потребители:** {{kto_vyzyvaet_api}} (web-фронтенд, мобильные приложения, партнёры) - **Ограничения:** {{ograniceniia}} (стек, нагрузка, обратная совместимость — необязательно) ## Что спроектировать **1. Ресурсная модель** - Ресурсы и их иерархия, выбор между вложенными путями и плоскими с фильтрами - Имена: множественное число, kebab-case, без глаголов в путях - Действия, не ложащиеся в CRUD: под-ресурсы или POST на действие — с обоснованием **2. Контракты** - Таблица маршрутов: метод, путь, назначение, статус-коды успеха и ошибок - Пагинация (cursor против offset — выбери и обоснуй), сортировка, фильтрация - Идемпотентность: какие операции требуют Idempotency-Key **3. Ошибки и версионирование** - Единый формат ошибки: код машинный, сообщение человеческое, поле detail - Стратегия версионирования: путь /v1 против заголовков — под потребителей из вводных - Что считается ломающим изменением и как его катить ## Формат ответа 1. **Ресурсная модель** — списком, с обоснованием спорных решений. 2. **Таблица маршрутов** — метод, путь, статус-коды. 3. **Примеры** — 3–4 пары запрос/ответ JSON для ключевых сценариев, включая ошибку валидации и 404. 4. **Открытые вопросы** — что нужно решить до реализации.
Заполните переменные

Пример запуска

Сгенерировано Claude Opus 5.5, 24 сентября 2026 г.

Значения переменных

Описание сервиса
CoWorkBook — сервис бронирования переговорных комнат и рабочих мест в сети коворкингов компании «Открытое Пространство». Сеть насчитывает 12 площадок в Москве, Санкт-Петербурге и Казани, на каждой площадке от 4 до 15 переговорных комнат разной вместимости (2–20 человек) и от 30 до 120 индивидуальных рабочих мест. Сервис решает три задачи: 1. Резиденты (сотрудники компаний-арендаторов) и гости бронируют переговорные комнаты и рабочие места на определённые временные слоты, видят доступность в реальном времени, могут продлить, перенести или отменить бронь. 2. Администраторы коворкинга управляют расписанием, блокируют комнаты на обслуживание, настраивают тарифы (часть комнат платная сверх абонемента) и следят за загрузкой площадок. 3. Система контроля доступа (RFID-турникеты и электронные замки на дверях переговорных) автоматически открывает дверь за 5 минут до начала подтверждённой брони и закрывает доступ через 10 минут после окончания, если бронь не продлена. Пиковая нагрузка приходится на будние дни с 9:00 до 11:00, когда сотрудники массово бронируют переговорные на весь рабочий день; в это время число запросов на создание брони может кратно превышать средний уровень. Для платных бронирований (переговорные премиум-класса, аренда сверх лимита абонемента) после подтверждения брони выставляется счёт через эквайринг. При отсутствии свободных слотов пользователь может встать в лист ожидания и получить уведомление, если бронь освободится.
Сущности и связи
Coworking (1—N) Room; Room (1—N) Booking; Organization (1—N) User; User (1—N) Booking как владелец; Booking (1—N) Participant (приглашённые User); Booking (0—1) Invoice для платных броней; Room (1—N) WaitlistEntry, когда слот занят.
Кто вызывает api
Веб-фронтенд (React SPA) для резидентов и админов коворкинга; мобильные приложения iOS и Android для резидентов; партнёрские HR-системы компаний-арендаторов (синхронизация списка сотрудников); RFID-контроллеры дверей и турникетов, дёргающие API/вебхуки при проходе.
Ограничения
Стек: Node.js (NestJS) + PostgreSQL, деплой в Kubernetes. Пиковая нагрузка до 500 RPS в будни 9:00–11:00. Мобильные приложения используют API с версии 1.0 — обратная совместимость обязательна не менее 6 месяцев после анонса breaking change. SLA 99.9%. Персональные данные резидентов должны храниться на серверах в РФ (152-ФЗ).

CoWorkBook API v1: проектирование

0. Ключевые решения

Решение Выбор Почему
Бронируемая единица Единый ресурс spaces с полем kind: meeting_room | desk Брони, доступность и лист ожидания устроены одинаково для комнат и мест. Два параллельных дерева API удвоили бы маршруты и логику клиентов.
Иерархия Не глубже одного уровня вложенности. Канонический путь каждого ресурса плоский. «Мои брони на всех площадках» с вложенными путями не получить. Плоские пути с фильтрами покрывают все срезы.
Не-CRUD действия Под-ресурс-существительное: cancellation, extensions, check-ins, acceptance У действия есть свои данные (причина, новое время, штраф) и история. Путь остаётся без глаголов.
Пагинация Cursor Брони активно вставляются в пик. С offset страницы «плывут» и дают дубли и пропуски. Лента для RFID по сути change feed.
Идентификаторы Непрозрачные строки с префиксом: bkg_01J9…, spc_… Нельзя перебрать ID. Саппорт по префиксу сразу видит тип сущности. Схему хранения можно менять.
Ошибки RFC 9457 application/problem+json плюс машинное поле code Стандарт, но клиенты ветвятся по code, а не по тексту.
Версионирование В пути: /v1 Подходит мобильным клиентам, RFID-прошивкам и маршрутизации в Ingress. Подробности в разделе 4.
Время RFC 3339 со смещением. У площадки есть timezone. Слоты кратны 15 минутам. Москва, Петербург и Казань сегодня в UTC+3, но на это нельзя закладываться.
Деньги {"amount": "1500.00", "currency": "RUB"}, сумма строкой Нет ошибок округления float в JS и Swift.

1. Ресурсная модель

Справочники площадок

  • coworkings: площадки. Их 12, пагинация номинальная, но формат конверта тот же, что у остальных списков.
  • spaces: комнаты и рабочие места. Создаются вложенно (POST /coworkings/{id}/spaces), потому что вне площадки не существуют. Читаются и изменяются по плоскому /spaces/{id}: брони и ссылки из них ссылаются на space без контекста площадки.
    • Спорное решение: в доменной модели сущность называется Room, а в API ресурс называется spaces. Отображение Room → space(kind=meeting_room) фиксируем в глоссарии. Если бизнес настаивает на терминологии, откат дешёвый, пока нет клиентов (см. открытые вопросы).
    • Удаления нет. Используется PATCH status=archived: на space ссылаются исторические брони и счета.
  • availability: вычисляемый ресурс только для чтения.
    • /spaces/{id}/availability отдаёт слоты одного пространства.
    • /availability?coworking_id=… ищет свободные пространства по критериям.
    • Это существительное, а не /search-free-rooms: ответ описывает состояние, а не выполняет действие.

Бронирование

  • bookings: плоский ресурс с фильтрами owner_id, participant_id, space_id, coworking_id, status и диапазоном времени. owner_id=me работает как алиас.
  • Машина состояний брони:
    confirmed ──► checked_in ──► completed
        │                 
        ├──► cancelled    
        └──► no_show        (если check-in обязателен, см. открытые вопросы)
    
  • Действия над бронью:
    • POST /bookings/{id}/cancellation, а не DELETE.
      • Бронь не исчезает: она остаётся в истории и в отчётах загрузки, на ней может висеть штраф за позднюю отмену.
      • У отмены есть тело (reason) и результат (refund, fee).
      • DELETE с телом плохо поддерживается прокси и SDK.
      • Семантически это создание синглтон-подресурса «отмена». Повторный вызов возвращает ту же отмену.
    • POST /bookings/{id}/extensions: продление.
      • Это отдельный ресурс, а не PATCH end_at. Продление имеет свою бизнес-логику: проверку следующего слота, доплату и немедленное обновление гранта на замке.
      • Каждое продление сохраняется, это нужно для биллинга и разборов «почему дверь открылась».
    • PATCH /bookings/{id}: перенос (start_at, end_at, space_id) и мета-поля. Обычное изменение состояния ресурса, требует If-Match.
    • POST /bookings/{id}/check-ins: фиксация прихода. Обычно создаётся автоматически событием RFID, но доступна и вручную из приложения.
  • participants: вложенный ресурс /bookings/{id}/participants. Вне брони смысла не имеет. Участники получают доступ к двери.

Оплата

  • invoices:
    • Плоский ресурс для админов и финансов.
    • Синглтон /bookings/{id}/invoice для клиента.
    • Счёт создаётся асинхронно после подтверждения брони, поэтому эквайринг не замедляет POST /bookings в пик.
  • payment-sessions: POST /invoices/{id}/payment-sessions возвращает URL или токен платёжной формы эквайера.
  • Вебхук эквайера POST /webhooks/acquiring/{provider} не входит в публичный контракт. Проверяется подпись, обработка идемпотентна по ID платежа провайдера.

Лист ожидания

  • waitlist-entries: плоский ресурс с полями space_id (или критерии coworking_id + min_capacity) и желаемым интервалом.
  • Когда слот освобождается, запись переходит в статус offered с offer_expires_at. Пользователь подтверждает через POST /waitlist-entries/{id}/acceptance, и создаётся бронь.
  • Почему не автобронирование: пользователь мог уже решить проблему иначе. Лишние автоброни превратились бы в no-show и лишние счета.

Администрирование

  • maintenance-blocks: блокировки на обслуживание. Плоский ресурс, так как один блок может покрывать несколько пространств, например весь этаж.
  • tariffs: тарифы, на них ссылается space.tariff_id.
  • occupancy: /coworkings/{id}/occupancy, вычисляемый отчёт о загрузке.

Организации и HR-синхронизация

  • organizations, users. Пользователи видны через /organizations/{id}/users, а /users/{id} остаётся каноническим путём. /users/me возвращает текущего пользователя.
  • Upsert по внешнему ID HR-системы: PUT /organizations/{org_id}/users/by-external-id/{external_id}.
    • Идемпотентен по природе, поэтому Idempotency-Key не нужен.
    • Партнёр не хранит наши ID.
  • Массовая синхронизация: POST /organizations/{org_id}/user-sync-jobs возвращает 202, статус проверяется через GET /user-sync-jobs/{id}.
    • Полный снимок на 5 000 сотрудников нельзя обработать синхронно в пределах таймаута Ingress.

Контроль доступа (RFID)

Основная модель: контроллер хранит локальный кэш грантов, и онлайн-запрос остаётся запасным вариантом. SLA 99.9% допускает около 43 минут простоя в месяц. Дверь переговорной не должна зависеть от доступности API в момент прохода.

  • GET /access-points/{id}/grants?cursor=…: change feed грантов.
    • Грант содержит card_uid, valid_from = start_at − 5 мин и valid_until = end_at + 10 мин.
    • Изменения (продление, отмена, новый участник) приходят в ленте как upsert или delete.
  • POST /access-decisions: онлайн-проверка «пустить ли карту X в точку Y».
    • Всегда возвращает 200 с decision: allow|deny. Отказ в доступе является нормальным бизнес-результатом, а не ошибкой клиента. 403 путал бы «контроллер не авторизован» и «карта не пускается».
  • POST /access-events: пакетная отправка журнала проходов, ответ 202. Дедупликация по event_id, который генерирует контроллер. Проходы автоматически создают check-ins.

Аутентификация (коротко, влияет на контракты)

  • Пользователи: OIDC, access token JWT.
  • HR-партнёры: OAuth2 client credentials с доступом только к своей organization.
  • RFID-контроллеры: mTLS с сертификатом устройства. Устройство привязано к своим access-points.
  • Чужие ресурсы: если у пользователя нет доступа к чужой брони, возвращается 404, а не 403, чтобы не раскрывать существование ресурса. 403 остаётся для случаев «ресурс вам виден, но действие запрещено».

2. Контракты

2.1 Таблица маршрутов

Для всех маршрутов дополнительно возможны: 400 (некорректный JSON или параметры), 401, 403, 429 (с Retry-After) и 503. В таблице указаны специфичные коды. IK означает, что заголовок Idempotency-Key обязателен.

Площадки и пространства

Метод Путь Назначение Успех Ошибки
GET /v1/coworkings Список площадок 200 —
GET /v1/coworkings/{coworking_id} Площадка 200, 304 404
PATCH /v1/coworkings/{coworking_id} Изменить (админ) 200 404, 412, 422, 428
GET /v1/coworkings/{coworking_id}/spaces Пространства площадки (kind, min_capacity, status) 200 404
POST /v1/coworkings/{coworking_id}/spaces Создать пространство (админ) 201 + Location 404, 409 space_code_taken, 422
GET /v1/spaces/{space_id} Пространство 200, 304 404
PATCH /v1/spaces/{space_id} Изменить или архивировать 200 404, 409 space_has_future_bookings, 412, 422, 428
GET /v1/spaces/{space_id}/availability Слоты за from–to (не более 31 дня) 200 404, 422
GET /v1/availability Поиск свободного: coworking_id, kind, min_capacity, from, to 200 422
GET /v1/coworkings/{coworking_id}/occupancy Загрузка, granularity=hour|day (админ) 200 404, 422

Брони

Метод Путь Назначение Успех Ошибки
GET /v1/bookings Список с фильтрами 200 422
POST /v1/bookings Создать бронь (IK) 201 + Location 404 space_not_found, 409 slot_unavailable, 409 idempotency_request_in_progress, 422 validation_failed / quota_exceeded / idempotency_key_reused, 428 idempotency_key_required
GET /v1/bookings/{booking_id} Бронь 200, 304 404
PATCH /v1/bookings/{booking_id} Перенос и мета-поля (If-Match обязателен) 200 404, 409 slot_unavailable / booking_not_modifiable, 412, 422, 428
POST /v1/bookings/{booking_id}/cancellation Отмена (IK) 200 (повторно тоже 200) 404, 409 booking_not_cancellable
POST /v1/bookings/{booking_id}/extensions Продление (IK) 201 404, 409 slot_unavailable / booking_not_active, 422
POST /v1/bookings/{booking_id}/check-ins Check-in 201; 200, если уже отмечен 404, 409 check_in_window_closed
GET /v1/bookings/{booking_id}/participants Участники 200 404
POST /v1/bookings/{booking_id}/participants Добавить участника (IK) 201 404, 409 already_participant, 422 capacity_exceeded
DELETE /v1/bookings/{booking_id}/participants/{user_id} Убрать участника 204 404
GET /v1/bookings/{booking_id}/invoice Счёт брони 200 404 invoice_not_found

Оплата

Метод Путь Назначение Успех Ошибки
GET /v1/invoices Счета (status, organization_id, период) 200 422
GET /v1/invoices/{invoice_id} Счёт 200 404
POST /v1/invoices/{invoice_id}/payment-sessions Сессия оплаты у эквайера (IK) 201 404, 409 invoice_already_paid, 502 acquiring_unavailable
POST /v1/webhooks/acquiring/{provider} Колбэк эквайера (внутренний) 200 401 invalid_signature

Лист ожидания

Метод Путь Назначение Успех Ошибки
GET /v1/waitlist-entries Мои записи или записи по пространству (админ) 200 422
POST /v1/waitlist-entries Встать в очередь (IK) 201 404, 409 slot_available / already_waitlisted, 422
GET /v1/waitlist-entries/{entry_id} Запись 200 404
DELETE /v1/waitlist-entries/{entry_id} Выйти из очереди 204 404
POST /v1/waitlist-entries/{entry_id}/acceptance Принять предложение, создать бронь (IK) 201 (тело содержит booking) 404, 409 offer_expired / offer_not_active

Администрирование

Метод Путь Назначение Успех Ошибки
GET /v1/maintenance-blocks Блокировки 200 422
POST /v1/maintenance-blocks Заблокировать (IK; conflict_policy=reject|cancel_bookings) 201 404, 409 conflicting_bookings, 422
PATCH /v1/maintenance-blocks/{block_id} Изменить 200 404, 409, 412, 422
DELETE /v1/maintenance-blocks/{block_id} Снять блокировку 204 404
GET /v1/tariffs Тарифы 200 —
POST /v1/tariffs Создать тариф 201 422
PATCH /v1/tariffs/{tariff_id} Изменить тариф (действует на новые брони) 200 404, 412, 422

Организации и пользователи

Метод Путь Назначение Успех Ошибки
GET /v1/users/me Профиль, квоты абонемента 200 —
GET /v1/users/{user_id} Пользователь 200 404
GET /v1/organizations/{org_id}/users Сотрудники организации 200 404
PUT /v1/organizations/{org_id}/users/by-external-id/{external_id} Upsert из HR 201 (создан) / 200 (обновлён) 404, 412, 422
DELETE /v1/organizations/{org_id}/users/by-external-id/{external_id} Деактивация, будущие брони отменяются 204 404
POST /v1/organizations/{org_id}/user-sync-jobs Массовая синхронизация (IK) 202 + Location 404, 413, 422
GET /v1/user-sync-jobs/{job_id} Статус и отчёт синхронизации 200 404

Контроль доступа и служебное

Метод Путь Назначение Успех Ошибки
GET /v1/access-points/{access_point_id}/grants Change feed грантов (cursor) 200 404, 410 cursor_expired (нужна полная ресинхронизация)
POST /v1/access-decisions Онлайн-решение по карте 200 (allow/deny) 404 access_point_not_found, 422
POST /v1/access-events Пакет журнала проходов (до 500 штук) 202 422
GET /v1/client-config Минимальные версии клиентов, флаги 200 —

2.2 Пагинация, сортировка, фильтрация

Cursor-пагинация везде, где есть список. Почему не offset:

  • Сдвиг выборки. В пик между запросами страниц вставляются сотни броней, и OFFSET даёт дубли и пропуски.
  • Производительность. OFFSET 10000 в PostgreSQL читает и отбрасывает строки. Keyset-запрос WHERE (start_at, id) > ($1, $2) работает по индексу.
  • Change feed. Для ленты грантов RFID курсор является единственным корректным вариантом.
  • Цена решения. Админка теряет «перейти на страницу 17». Это приемлемо: админы ищут фильтрами по дате и площадке, а не листают страницы.

Контракт:

GET /v1/bookings?coworking_id=cwk_01J8…&status=confirmed,checked_in
                &start_from=2026-09-24T00:00:00+03:00&start_to=2026-09-25T00:00:00+03:00
                &sort=start_at&limit=50&cursor=eyJrIjpbIjIwMjYtMDkt…
{
  "data": [ { "id": "bkg_01J9…", "…": "…" } ],
  "page": { "next_cursor": "eyJrIjpbIjIwMjYtMDkt…", "has_more": true }
}
  • limit: по умолчанию 50, максимум 200. Больше 200 возвращает 422, а не молча обрезается.
  • Курсор непрозрачный: base64 от (ключ сортировки, id, хэш фильтров). Если курсор применить с другими фильтрами, вернётся 422 cursor_mismatch.
  • total по умолчанию не считается, так как COUNT(*) в пик обходится дорого. Можно запросить опцией ?include=total для админских отчётов.
  • Сортировка: sort=start_at или sort=-created_at. Поля берутся из whitelist, для каждого поля есть индекс. id добавляется неявно как tie-breaker.
  • Фильтры:
    • Плоские query-параметры, мультизначения через запятую.
    • Диапазоны задаются суффиксами _from и _to, полуинтервал [from, to).
    • Неизвестный фильтр возвращает 422, а не игнорируется: игнорирование фильтра по опечатке опасно.
  • PII не передаётся в query: email и телефон не ставятся в URL, потому что URL попадают в логи Ingress. Поиск пользователей идёт по ID или через POST /v1/users/search (открытый вопрос).

2.3 Идемпотентность

Операция Нужен IK? Почему
POST /bookings Обязателен (иначе 428) Мобильная сеть и ретраи в пик. Двойная бронь означает двойной счёт.
POST …/extensions, …/cancellation, …/acceptance Обязателен Деньги и изменение доступа к двери.
POST …/payment-sessions, POST /maintenance-blocks, POST …/participants, POST …/user-sync-jobs, POST /waitlist-entries Обязателен Побочные эффекты: эквайринг, уведомления, массовые отмены.
POST /access-events Нет Дедупликация по event_id каждого события в пакете.
POST /access-decisions Нет Операция только читает.
PUT, DELETE Нет Идемпотентны по семантике HTTP.
PATCH Нет, но If-Match обязателен Защита от lost update: админ и пользователь одновременно переносят бронь.

Правила хранения ключа:

  • Ключ — UUID от клиента.
  • Область действия: (субъект, метод, путь), срок хранения 24 часа.
  • Хранится вместе с хэшем тела и сохранённым ответом.

Поведение при повторах:

  • Повтор с тем же телом: возвращается сохранённый ответ (тот же статус, заголовок Idempotent-Replayed: true).
  • Тот же ключ, другое тело: 422 idempotency_key_reused.
  • Первый запрос ещё выполняется: 409 idempotency_request_in_progress с Retry-After: 1.

Защита от двойного бронирования не опирается только на IK. В PostgreSQL стоит exclusion constraint:

EXCLUDE USING gist (space_id WITH =, tstzrange(start_at, end_at) WITH &&)
  WHERE (status IN ('confirmed','checked_in'))

Гонку двух пользователей за один слот решает БД, проигравший получает 409 slot_unavailable. Maintenance-blocks хранятся в той же таблице занятости, поэтому покрываются тем же ограничением.

Нагрузка 500 RPS.

  • availability кэшируется на 5–10 секунд с ETag, запись брони инвалидирует кэш.
  • POST /bookings не ходит во внешние системы: счёт, уведомления и гранты создаются через outbox-очередь.
  • Rate limit на пользователя отдаётся с заголовками RateLimit-*.

3. Ошибки и версионирование

3.1 Единый формат ошибки

Content-Type: application/problem+json. Поля:

Поле Для кого Стабильность
code Машина, клиент ветвится по нему Часть контракта, не меняется внутри версии
title Человек, короткое описание класса ошибки, локализуется по Accept-Language Может меняться
detail Человек, объяснение конкретного случая Может меняться
status Дублирует HTTP-статус —
type URI документации по code Стабилен
instance Путь запроса —
trace_id Саппорт, корреляция с логами и трейсами —
errors[] Ошибки полей: pointer (JSON Pointer), code, detail code стабилен
Расширения Контекст ошибки, например alternatives в slot_unavailable Документируются для каждого code

Правила:

  • Разделение 400 и 422. 400 означает, что запрос синтаксически нельзя разобрать. 422 означает, что запрос разобран, но нарушает правила домена или валидации.
  • Неизвестный code. Клиент обязан уметь обработать новый code через fallback по HTTP-статусу. Благодаря этому добавление кода не ломает контракт.
  • Нет утечек реализации. Стектрейсы и SQL-ошибки в ответ не попадают. На 5xx отдаётся code: internal_error и trace_id.

3.2 Версионирование: /v1 в пути

Почему путь, а не заголовок (Accept: application/vnd.cowork.v2+json):

  • Мобильные клиенты. Версия видна в каждом URL, в логах и в аналитике. Ошибиться, забыв заголовок в одном из сотен вызовов, нельзя.
  • RFID-контроллеры. Прошивки вендоров часто позволяют настроить только base URL, а произвольные заголовки не поддерживают.
  • Инфраструктура. Ingress и rate limiting маршрутизируют по префиксу. При необходимости v2 выносится в отдельный Deployment одной строкой. Кэширование работает без Vary.
  • Цена решения. Мажорная версия меняет все URL. Поэтому мажорные версии выпускаются редко, а эволюция внутри v1 идёт аддитивно.

Дополнительно:

  • Клиенты отправляют User-Agent: CoWorkBook-iOS/1.4.2. Метрики по версиям нужны, чтобы решать, когда выключать старое.
  • GET /v1/client-config возвращает min_supported_version. Приложение само показывает экран «обновитесь», сервер не ломает старых клиентов.
  • Внутри NestJS одна доменная модель. v1 и v2 реализуются как слои контроллеров и DTO-мапперов, а не две копии бизнес-логики.

3.3 Что ломающее, что нет

Ломающее, требует новой мажорной версии или отдельного поля:

  • удаление или переименование поля, эндпоинта или параметра;
  • смена типа или формата поля ("1500.00" на 150000, ID-строка на число);
  • новое обязательное поле или параметр в запросе;
  • ужесточение валидации (например, минимальная длительность брони 30 минут вместо 15);
  • смена статус-кода или code ошибки для существующей ситуации;
  • смена семантики поля или умолчаний: сортировка по умолчанию, уменьшение максимального limit, смысл статуса;
  • смена требований аутентификации.

Не ломающее, при условии что это правило задокументировано с первого дня:

  • новые необязательные поля в ответе (клиенты обязаны игнорировать неизвестные поля);
  • новые значения enum. Клиенты обязаны корректно обрабатывать неизвестный status или kind. Это требование в явном виде фиксируется для iOS и Android: иначе добавление kind: phone_booth уронит декодер в Swift;
  • новые эндпоинты, необязательные параметры, новые code ошибок.

Как выкатывать breaking change (минимум 6 месяцев по требованиям):

  1. Сначала пытаемся сделать изменение аддитивным: новое поле рядом со старым (price и price_v2), старое помечаем deprecated в OpenAPI.
  2. Если это невозможно, выпускаем /v2 параллельно /v1.
  3. Анонс (T0). Changelog, письмо партнёрам. В ответах v1 появляются заголовки:
    • Deprecation (RFC 9745);
    • Sunset: <дата ≥ T0 + 6 мес> (RFC 8594);
    • Link: <…/migration-guide>; rel="deprecation".
  4. Мониторинг. Дашборд трафика v1 по User-Agent и client_id. Партнёрам и вендору RFID пишем адресно.
  5. T0 + 5 мес. Поднимаем min_supported_version в client-config для мобильных приложений.
  6. Sunset. Старый эндпоинт отвечает 410 Gone с code: api_version_sunset, но только если остаточный трафик пренебрежимо мал. Иначе продлеваем срок: SLA дороже чистоты.

4. Примеры

4.1 Создание платной брони: успех

POST /v1/bookings
Authorization: Bearer eyJ…
Idempotency-Key: 5f1c2b7e-9a41-4c3e-8d0a-6b2f7e1d9c33
Content-Type: application/json

{
  "space_id": "spc_01J8Z3K4QW7M2N",
  "start_at": "2026-09-25T10:00:00+03:00",
  "end_at":   "2026-09-25T12:00:00+03:00",
  "title": "Планирование Q4",
  "participant_ids": ["usr_01J7AAB1", "usr_01J7AAC9"]
}
HTTP/1.1 201 Created
Location: /v1/bookings/bkg_01J9F2X8R4T6YP
ETag: "3"
Content-Type: application/json

{
  "id": "bkg_01J9F2X8R4T6YP",
  "status": "confirmed",
  "space": { "id": "spc_01J8Z3K4QW7M2N", "name": "Нева", "kind": "meeting_room", "capacity": 8 },
  "coworking_id": "cwk_01J8SPB02",
  "owner_id": "usr_01J7AAA0",
  "start_at": "2026-09-25T10:00:00+03:00",
  "end_at":   "2026-09-25T12:00:00+03:00",
  "title": "Планирование Q4",
  "participants_count": 3,
  "access": {
    "valid_from":  "2026-09-25T09:55:00+03:00",
    "valid_until": "2026-09-25T12:10:00+03:00"
  },
  "pricing": {
    "billable": true,
    "reason": "premium_room",
    "total": { "amount": "3000.00", "currency": "RUB" }
  },
  "invoice": { "id": null, "status": "pending_issue" },
  "created_at": "2026-09-24T09:14:03+03:00",
  "updated_at": "2026-09-24T09:14:03+03:00"
}

invoice.id = null: счёт выставится асинхронно. Клиент узнает об этом по push-уведомлению или повторному GET.

4.2 Ошибка валидации

POST /v1/bookings
Idempotency-Key: 0b8e…
Content-Type: application/json

{
  "space_id": "spc_01J8Z3K4QW7M2N",
  "start_at": "2026-09-25T10:10:00+03:00",
  "end_at":   "2026-09-25T09:00:00+03:00",
  "participant_ids": ["usr_01", "usr_02", "usr_03", "usr_04", "usr_05", "usr_06", "usr_07", "usr_08"]
}
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json

{
  "type": "https://api.coworkbook.ru/errors/validation_failed",
  "code": "validation_failed",
  "title": "Некорректные параметры брони",
  "status": 422,
  "detail": "Исправьте отмеченные поля и повторите запрос.",
  "instance": "/v1/bookings",
  "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
  "errors": [
    {
      "pointer": "/start_at",
      "code": "slot_not_aligned",
      "detail": "Время начала должно быть кратно 15 минутам (например, 10:00 или 10:15)."
    },
    {
      "pointer": "/end_at",
      "code": "must_be_after_start",
      "detail": "Время окончания должно быть позже времени начала."
    },
    {
      "pointer": "/participant_ids",
      "code": "capacity_exceeded",
      "detail": "Вместимость комнаты «Нева» 8 человек, а с организатором получается 9."
    }
  ]
}

Ошибки возвращаются все сразу, а не по одной: иначе форма в SPA потребует нескольких раундов исправлений.

4.3 Слот занят: конфликт с подсказками

HTTP/1.1 409 Conflict
Content-Type: application/problem+json

{
  "type": "https://api.coworkbook.ru/errors/slot_unavailable",
  "code": "slot_unavailable",
  "title": "Слот уже занят",
  "status": 409,
  "detail": "Комната «Нева» занята 25.09 с 10:00 до 12:00. Выберите другое время или встаньте в лист ожидания.",
  "instance": "/v1/bookings",
  "trace_id": "a1e0c3d9f22b4b7e9c0e51d6a8b3f004",
  "alternatives": [
    { "space_id": "spc_01J8Z3K4QW7M2N", "start_at": "2026-09-25T12:00:00+03:00", "end_at": "2026-09-25T14:00:00+03:00" },
    { "space_id": "spc_01J8Z3K5LADOGA", "start_at": "2026-09-25T10:00:00+03:00", "end_at": "2026-09-25T12:00:00+03:00" }
  ],
  "waitlist": {
    "available": true,
    "href": "/v1/waitlist-entries"
  }
}

4.4 Бронь не найдена

GET /v1/bookings/bkg_01J9NOTEXIST00
Authorization: Bearer eyJ…
HTTP/1.1 404 Not Found
Content-Type: application/problem+json

{
  "type": "https://api.coworkbook.ru/errors/booking_not_found",
  "code": "booking_not_found",
  "title": "Бронь не найдена",
  "status": 404,
  "detail": "Бронь bkg_01J9NOTEXIST00 не существует или недоступна для вашей учётной записи.",
  "instance": "/v1/bookings/bkg_01J9NOTEXIST00",
  "trace_id": "0af7651916cd43dd8448eb211c80319c"
}

Формулировка «не существует или недоступна» выбрана намеренно: чужая бронь неотличима от несуществующей.


5. Открытые вопросы (решить до реализации)

Бизнес-правила

  1. Предоплата или постоплата. По вводным счёт выставляется после подтверждения. Открывается ли дверь по неоплаченной платной брони? Если нет, нужен статус pending_payment с TTL удержания слота. Это меняет машину состояний и должно быть решено до релиза v1, иначе добавление статуса ломает клиентов.
  2. No-show и освобождение слота. Если check-in по RFID не произошёл за N минут, бронь освобождается и отдаётся листу ожидания? Какое N? Это главный источник свободных слотов в пик.
  3. Политика отмены. Окно бесплатной отмены, штрафы, возвраты через эквайринг. Влияет на ответ cancellation.
  4. Лист ожидания. FIFO или приоритеты по тарифу? TTL предложения (предлагаю 10 минут)? Предлагается слот одному или сразу нескольким? Допускается ли частичное совпадение интервала?
  5. Повторяющиеся брони («каждый вторник 10:00») — типичный сценарий для переговорных. Если нужен, это отдельный ресурс booking-series. Лучше заложить его в v1, чем прикручивать позже.
  6. Квоты абонемента. Лимит на пользователя или на организацию? В часах или в деньгах? Как считается «сверх лимита» при продлении, которое пересекает границу?
  7. maintenance-blocks поверх существующих броней. Кто и как уведомляет пострадавших? Нужна ли автоматическая пересадка в аналогичную комнату?

Идентичность и данные

  1. Гости. Есть ли у гостя учётная запись? Как он получает доступ к двери (одноразовый QR, временная карта)? Какие ПД гостя собираем и сколько храним?
  2. Терминология: spaces или раздельные rooms и desks. Нужно согласие продукта, пока клиентов нет.
  3. HR-синхронизация.
    • Кто источник истины для сотрудников?
    • Что делать с будущими бронями уволенного: отменять или передавать руководителю?
    • Поддерживаем ли SCIM 2.0 вместо своего формата, чтобы партнёрам не писать интеграцию?

Инфраструктура и 152-ФЗ

  1. 152-ФЗ и сторонние каналы.
    • Push-уведомления через APNs и FCM и SDK аналитики передают данные за рубеж. В payload не должно быть ПД (имён, названий встреч), только ID и тип события.
    • Эквайер и провайдер SMS или email должны хранить данные в РФ.
    • Логи и трейсинг (URL, тела запросов) тоже содержат ПД: нужна маскировка и хранение в российском контуре.
  2. RFID-вендор.
    • Поддерживают ли контроллеры локальный кэш грантов и pull change feed?
    • Или они умеют только онлайн-запрос? Тогда access-decisions становится критичным путём с жёстким SLO по латентности (p99 < 200 мс) и отдельным пулом подов.
    • Как синхронизируется время на контроллерах (NTP)? От этого зависят окна «−5 / +10 минут».
  3. Доступность в реальном времени. SSE (GET /v1/availability/stream), WebSocket или polling с ETag? Для SPA проще всего SSE, для мобильных приложений — push плюс refetch. Нужно решить, чтобы не держать тысячи соединений в пик без нужды.
  4. Часовые пояса. Сейчас все площадки живут в UTC+3. Нужны ли гарантии на случай открытия площадки в другом поясе (например, Екатеринбург)? Модель уже поддерживает это через coworking.timezone, но в UI и отчётах это нужно проверить.

Похожие промпты

Программирование и Разработка

Ревью pull request по diff

Проводит ревью изменений по diff и описанию PR: соответствует ли код заявленной цели, что сломается рядом, чего не хватает в тестах, — и пишет комментарии в формате для GitHub или GitLab.

{} 3 перем.
Программирование и Разработка

Ревью Python-кода: типизация и идиомы

Проводит ревью Python-кода: ошибки, которые проявятся в рантайме, аннотации типов, идиоматичность, обработка исключений и ресурсов — с правками «до/после».

{} 3 перем.
Программирование и Разработка

Ревью SQL-запросов и миграций

Проверяет SQL-запросы и миграции схемы: корректность на граничных данных, индексы под реальные выборки, блокировки при миграции на живой таблице и откат.

{} 3 перем.
Программирование и Разработка

Ревью Next.js-кода: App Router и Server Components

Проводит ревью Next.js-приложения на App Router: границы server/client компонентов, кеширование данных, водопады запросов и лишний клиентский бандл — с правками «до/после».

{} 4 перем.