Проектировщик REST API
Проектирует REST API от ресурсной модели до спецификации: маршруты, статус-коды, пагинация, версионирование и формат ошибок — с примерами запросов и ответов.
Пример запуска
Сгенерировано 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 ссылаются исторические брони и счета.
- Спорное решение: в доменной модели сущность называется Room, а в API ресурс называется
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 месяцев по требованиям):
- Сначала пытаемся сделать изменение аддитивным: новое поле рядом со старым (
priceиprice_v2), старое помечаемdeprecatedв OpenAPI. - Если это невозможно, выпускаем
/v2параллельно/v1. - Анонс (T0). Changelog, письмо партнёрам. В ответах
v1появляются заголовки:Deprecation(RFC 9745);Sunset: <дата ≥ T0 + 6 мес>(RFC 8594);Link: <…/migration-guide>; rel="deprecation".
- Мониторинг. Дашборд трафика
v1поUser-Agentиclient_id. Партнёрам и вендору RFID пишем адресно. - T0 + 5 мес. Поднимаем
min_supported_versionвclient-configдля мобильных приложений. - 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. Открытые вопросы (решить до реализации)
Бизнес-правила
- Предоплата или постоплата. По вводным счёт выставляется после подтверждения. Открывается ли дверь по неоплаченной платной брони? Если нет, нужен статус
pending_paymentс TTL удержания слота. Это меняет машину состояний и должно быть решено до релизаv1, иначе добавление статуса ломает клиентов. - No-show и освобождение слота. Если check-in по RFID не произошёл за N минут, бронь освобождается и отдаётся листу ожидания? Какое N? Это главный источник свободных слотов в пик.
- Политика отмены. Окно бесплатной отмены, штрафы, возвраты через эквайринг. Влияет на ответ
cancellation. - Лист ожидания. FIFO или приоритеты по тарифу? TTL предложения (предлагаю 10 минут)? Предлагается слот одному или сразу нескольким? Допускается ли частичное совпадение интервала?
- Повторяющиеся брони («каждый вторник 10:00») — типичный сценарий для переговорных. Если нужен, это отдельный ресурс
booking-series. Лучше заложить его вv1, чем прикручивать позже. - Квоты абонемента. Лимит на пользователя или на организацию? В часах или в деньгах? Как считается «сверх лимита» при продлении, которое пересекает границу?
maintenance-blocksповерх существующих броней. Кто и как уведомляет пострадавших? Нужна ли автоматическая пересадка в аналогичную комнату?
Идентичность и данные
- Гости. Есть ли у гостя учётная запись? Как он получает доступ к двери (одноразовый QR, временная карта)? Какие ПД гостя собираем и сколько храним?
- Терминология:
spacesили раздельныеroomsиdesks. Нужно согласие продукта, пока клиентов нет. - HR-синхронизация.
- Кто источник истины для сотрудников?
- Что делать с будущими бронями уволенного: отменять или передавать руководителю?
- Поддерживаем ли SCIM 2.0 вместо своего формата, чтобы партнёрам не писать интеграцию?
Инфраструктура и 152-ФЗ
- 152-ФЗ и сторонние каналы.
- Push-уведомления через APNs и FCM и SDK аналитики передают данные за рубеж. В payload не должно быть ПД (имён, названий встреч), только ID и тип события.
- Эквайер и провайдер SMS или email должны хранить данные в РФ.
- Логи и трейсинг (URL, тела запросов) тоже содержат ПД: нужна маскировка и хранение в российском контуре.
- RFID-вендор.
- Поддерживают ли контроллеры локальный кэш грантов и pull change feed?
- Или они умеют только онлайн-запрос? Тогда
access-decisionsстановится критичным путём с жёстким SLO по латентности (p99 < 200 мс) и отдельным пулом подов. - Как синхронизируется время на контроллерах (NTP)? От этого зависят окна «−5 / +10 минут».
- Доступность в реальном времени. SSE (
GET /v1/availability/stream), WebSocket или polling сETag? Для SPA проще всего SSE, для мобильных приложений — push плюс refetch. Нужно решить, чтобы не держать тысячи соединений в пик без нужды. - Часовые пояса. Сейчас все площадки живут в UTC+3. Нужны ли гарантии на случай открытия площадки в другом поясе (например, Екатеринбург)? Модель уже поддерживает это через
coworking.timezone, но в UI и отчётах это нужно проверить.
Похожие промпты
Ревью pull request по diff
Проводит ревью изменений по diff и описанию PR: соответствует ли код заявленной цели, что сломается рядом, чего не хватает в тестах, — и пишет комментарии в формате для GitHub или GitLab.
Ревью Python-кода: типизация и идиомы
Проводит ревью Python-кода: ошибки, которые проявятся в рантайме, аннотации типов, идиоматичность, обработка исключений и ресурсов — с правками «до/после».
Ревью SQL-запросов и миграций
Проверяет SQL-запросы и миграции схемы: корректность на граничных данных, индексы под реальные выборки, блокировки при миграции на живой таблице и откат.
Ревью Next.js-кода: App Router и Server Components
Проводит ревью Next.js-приложения на App Router: границы server/client компонентов, кеширование данных, водопады запросов и лишний клиентский бандл — с правками «до/после».