- -
- 100%
- +
– метрики (количество запросов, latency, коды ответов)
– трассировка (trace_id) – опционально, но полезно
Обязательные практики:
– request_id генерируется на входе и прокидывается дальше
– логируем: метод, путь, статус, время обработки, user_id (если есть), workspace_id (если есть)
Health checks
Обычно:
– /health – быстрый, без БД (жив ли процесс)
– /ready – проверка зависимостей (БД доступна, миграции применены)
Нюанс: readiness не должен “ддосить” базу. Проверки должны быть лёгкими.
7.6. Мини-итог главы
Мы договорились о вещах, которые делают API взрослым:
– ограничиваем частоту,
– защищаемся от повторов,
– пишем аудит,
– выбираем пагинацию,
– делаем наблюдаемость.
Теперь мы готовы к самой полезной инженерной привычке: сначала контракт, потом код.
Глава 8. API Contract (OpenAPI) – пишем до кода
Эта глава – про дисциплину, которая экономит недели: сначала описываем API как договор, а потом реализуем.
OpenAPI – это формат, который позволяет:
– задокументировать endpoints,
– описать схемы данных,
– зафиксировать ошибки,
– автоматически генерировать документацию и клиентов (если нужно).
Мы не будем вставлять огромный YAML на 30 страниц. Вместо этого зафиксируем структуру контракта, модели и правила, а также ключевые endpoints.
8.1. Основные принципы контракта
1) Стабильные модели ошибок (один формат на весь API).
2) Понятные коды ответа (не “всегда 200”).
3) Пагинация единым способом для всех списков.
4) Аутентификация единым способом.
5) Версионирование с первого дня.
И ещё правило, которое спасает нервы:
> Если вы не можете объяснить endpoint одной фразой – скорее всего, он делает слишком много.
8.2. Auth model (модель аутентификации)
Для простоты (и реальности) выбираем:
– Bearer token в заголовке Authorization: Bearer
Где токен берётся:
– из POST /auth/login (и, возможно, POST /auth/register сразу возвращает токен)
В OpenAPI это описывается как security scheme типа HTTP bearer.
Минимальные endpoints auth
– POST /auth/register
– POST /auth/login
– POST /auth/logout (опционально; зависит от того, храним ли сессии на сервере)
– GET /me (получить профиль текущего пользователя)
8.3. Error model (единая модель ошибок)
Самая частая боль API – когда ошибки везде разные. Мы сделаем единый формат.
Предлагаемая модель:
json
{
"error": {
"code": "validation_error",
"message": "Invalid request",
"details": [
{ "field": "email", "message": "Invalid format" }
],
"request_id": "req_123"
}
}
Где:
– code – машинно-обрабатываемый код (snake_case)
– message – коротко для человека
– details – массив деталей (опционально)
– request_id – чтобы найти запрос в логах
Типовые коды ошибок
– validation_error → HTTP 400
– unauthorized → 401
– forbidden → 403
– not_found → 404
– conflict → 409
– rate_limited → 429
– internal_error → 500
Важно: для 500 мы не раскрываем внутренности. Логи – для нас, клиенту – “internal_error”.
8.4. Pagination model (модель пагинации)
Мы выбрали cursor pagination для основных списков.
Запрос
Параметры:
– limit (по умолчанию 20, максимум например 100)
– cursor (опционально)
Пример:
GET /tasks?limit=20&cursor=eyJjcmVhdGVkX2F0IjoiLi4uIiwiaWQiOiIuLi4ifQ==
Ответ
Единый формат списка:
json
{
"items": [ … ],
"page": {
"next_cursor": "....",
"has_more": true
}
}
Правила:
– если has_more=false, next_cursor может быть null
– курсор непрозрачный для клиента (он не обязан понимать содержимое)
Если для некоторых endpoint’ов нужен offset – лучше не смешивать. Но если уж смешали, делайте разные endpoint’ы или разные модели ответа, чтобы клиент не гадал.
8.5. Versioning (версионирование)
Варианты:
– через путь: /api/v1/…
– через заголовок: Accept: application/vnd.taskflow.v1+json
Для простоты и ясности берём версию в пути:
– /api/v1
Почему:
– проще дебажить,
– проще проксировать,
– проще объяснить.
Правило:
– ломающее изменение – новая версия (/v2)
– не ломающее – расширяем текущую версию (добавляем поля, новые endpoints)
8.6. Endpoints: фиксируем основной набор
Ниже – список endpoint’ов, который покрывает домен из главы 6. Формат: метод, путь, смысл, основные ответы.
8.6.1. Auth / User
POST /api/v1/auth/register
Создать пользователя.
Request:
– password
– name (опционально)
Responses:
– 201 → создан пользователь (+ возможно токен)
– 400 validation_error
– 409 conflict (email занят)
– 429 rate_limited
POST /api/v1/auth/login
Логин.
Request:
– password
Responses:
– 200 → токен
– 400 validation_error
– 401 unauthorized
– 429 rate_limited
GET /api/v1/me
Текущий пользователь.
Responses:
– 200 user profile
– 401 unauthorized
8.6.2. Workspaces и участники
POST /api/v1/workspaces
Создать workspace.
Headers:
– Idempotency-Key (рекомендуется)
Responses:
– 201 workspace
– 401 unauthorized
– 400 validation_error
GET /api/v1/workspaces
Список workspace, где пользователь состоит.
Responses:
– 200 list (можно без пагинации, если их мало, но лучше с limit/cursor)
GET /api/v1/workspaces/{workspaceId}
Получить workspace.
Responses:
– 200
– 403 forbidden (если нет доступа)
– 404 not_found (можно вернуть 404 вместо 403, чтобы не “палить” существование)
POST /api/v1/workspaces/{workspaceId}/members
Добавить участника (owner/admin).
Request:
– user_email или user_id
– role (admin/member)
Responses:
– 201
– 403
– 404
– 409 (уже участник)
PATCH /api/v1/workspaces/{workspaceId}/members/{userId}
Изменить роль.
Responses:
– 200
– 403
– 409 (например, нельзя понизить owner “в никуда”)
DELETE /api/v1/workspaces/{workspaceId}/members/{userId}
Удалить участника.
Responses:
– 204
– 403
8.6.3. Projects
POST /api/v1/workspaces/{workspaceId}/projects
Создать проект.
Headers:
– Idempotency-Key (рекомендуется)
Responses:
– 201
– 403
– 400
GET /api/v1/workspaces/{workspaceId}/projects
Список проектов (с фильтром status=active|archived).
Responses:
– 200 paginated list
GET /api/v1/projects/{projectId}
Получить проект.
Responses:
– 200
– 403/404
PATCH /api/v1/projects/{projectId}
Обновить проект (name/description/status).
Responses:
– 200
– 400
– 403
8.6.4. Tasks
POST /api/v1/projects/{projectId}/tasks
Создать задачу в проекте.
Headers:
– Idempotency-Key (рекомендуется)
Request:
– title (required)
– description (optional)
– assignee_user_id (optional)
– priority (optional)
– due_date (optional)
– labels (optional: массив label_id)
Responses:
– 201 task
– 400 validation_error
– 403 forbidden
– 409 conflict (если идемпотентность конфликтует)
GET /api/v1/tasks/{taskId}
Получить задачу.
Responses:
– 200
– 403/404
PATCH /api/v1/tasks/{taskId}
Обновить задачу (частично).
Responses:
– 200
– 400
– 403
DELETE /api/v1/tasks/{taskId}
Удалить (или архивировать) задачу.
Responses:
– 204
– 403
GET /api/v1/workspaces/{workspaceId}/tasks
Список задач в workspace с поиском/фильтрами.
Query params (примерный набор):
– q – поиск по тексту
– project_id
– status
– assignee_user_id
– label_id
– sort (например created_at, updated_at, due_date)
– order (asc|desc)
– limit, cursor
Responses:
– 200 paginated list
8.6.5. Comments
POST /api/v1/tasks/{taskId}/comments
Добавить комментарий.
Headers:
– Idempotency-Key (можно, но не обязательно; полезно)
Request:
– body
Responses:
– 201
– 400
– 403
GET /api/v1/tasks/{taskId}/comments
Список комментариев (pagination).
Responses:
– 200
DELETE /api/v1/comments/{commentId}
Удалить комментарий (если разрешено правилами).
Responses:
– 204
– 403
8.6.6. Labels
POST /api/v1/workspaces/{workspaceId}/labels
Создать метку.
Responses:
– 201
– 400
– 403
– 409 (если имя метки уникально в workspace)
GET /api/v1/workspaces/{workspaceId}/labels
Список меток.
Responses:
– 200 (можно без пагинации, но лучше с limit/cursor)
8.6.7. Webhooks / Notifications (минимальный контракт)
Если делаем webhooks:
– POST /api/v1/workspaces/{workspaceId}/webhooks – создать подписку
– GET /api/v1/workspaces/{workspaceId}/webhooks – список
– DELETE /api/v1/webhooks/{webhookId} – удалить
Модель webhook:
– url
– events (например task.created, comment.created)
– secret (для подписи)
– is_active
Для email-уведомлений в учебной версии часто достаточно “внутренней отправки” без внешнего API. Но события всё равно должны быть в аудит/логах.
8.7. Коды ответов и “мелкая гигиена API”
Несколько правил, которые повышают доверие к API:
– POST создание → 201 Created (+ тело созданного ресурса)
– DELETE успешный → 204 No Content
– PATCH успешный → 200 OK (+ обновлённый ресурс)
– GET список → 200 OK + { items, page }
И ещё:
– даты/время – в ISO 8601 (2026-01-01T12:34:56Z)
– идентификаторы – строки (часто удобнее и переносимее)
– не возвращайте поля, которые клиент не должен видеть (пароли, секреты)
8.8. Что можно установить, чтобы удобно работать с OpenAPI
– Swagger UI или любая UI-обёртка для просмотра спецификации
– Postman/Insomnia – импортировать спецификацию и тестировать запросы
– Редактор YAML/JSON с подсветкой схем (любой нормальный IDE/редактор справится)
8.9. Мини-итог главы
Мы превратили требования продукта в набор понятных договорённостей:
– endpoints,
– модель ошибок,
– модель пагинации,
– модель аутентификации,
– версия API.
Теперь можно переходить к реализации на любом языке, не споря “как лучше назвать поле” на каждом шаге – всё уже зафиксировано контрактом.
Раздел III. База данных (одна на всех)
Глава 9. PostgreSQL: схема данных
Эта глава – про то, как спроектировать схему в PostgreSQL так, чтобы:
– API работало быстро и предсказуемо,
– данные не превращались в кашу через месяц,
– изменения можно было вносить без паники.
Мы будем держаться принципа: половина теории, половина практики. То есть: сначала понятные правила, затем – как это выглядит в схеме и что с этим делать руками.
9.1. Почему именно PostgreSQL (и что от него ожидать)
PostgreSQL – это “швейцарский нож” для бэкенда:
– умеет транзакции и строгую целостность;
– богатый SQL (CTE, оконные функции, JSON, полнотекст);
– отличные индексы и оптимизатор;
– работает одинаково хорошо и на ноутбуке, и на сервере.
Если вы планируете разрабатывать и учиться – Postgres благодарный. Он не делает вид, что всё в порядке, когда данные уже сломаны. Он честный: либо можно, либо нельзя.
Что установить для работы (по желанию):
– PostgreSQL (локально или в Docker)
– любой GUI-клиент: DBeaver, DataGrip, pgAdmin (что удобнее)
– psql (терминальный клиент; полезен как минимум для “быстро проверить”)
– Docker + docker-compose (чтобы одинаково запускать окружение)
9.2. Модель данных TaskFlow: от домена к таблицам
Напомним домен:
– User
– Workspace + участники (membership)
– Project
– Task
– Comment
– Label + связь task-label (many-to-many)
– Audit log
– Idempotency keys (для безопасного повтора POST)
Сразу полезное правило: таблица – это не “модель в коде”, а “факт в мире”.
Например, “участник workspace” – это факт, значит это отдельная таблица membership.
9.3. Нормализация: что это вообще и зачем вам это сегодня
Нормализация – это не ритуал. Это способ уменьшить вероятность того, что:
– вы храните одно и то же в пяти местах,
– оно расходится,
– и потом вы неделями чините отчёты и поиск.
9.3.1. Практическое правило №1: “Один факт – одно место”
Если роль пользователя в workspace хранится и в users, и в workspace_members, и в каком-то JSON – однажды вы поймаете ситуацию: “в одном месте admin, в другом member”. И вы проиграете спор с реальностью.
Роль в рамках workspace – это факт membership. Значит:
– workspace_members.role – единственный источник.
9.3.2. Практическое правило №2: “Массивы и списки – осторожно”
Например, хочется хранить метки как массив строк прямо в tasks.labels.
Это удобно первые два дня. Потом вы захотите:
– переименовать метку,
– вывести список всех меток workspace,
– посчитать статистику по меткам,
– не допускать дубликаты,
– индексировать фильтр по метке.
И внезапно вы понимаете, что метка – сущность, и у неё должна быть таблица.
Вывод: метки – отдельная таблица labels, а связь – task_labels.
9.3.3. Практическое правило №3: “Денормализация допустима, но осознанно”
Мы можем хранить workspace_id в tasks, даже если его можно вычислить через project. Это “снятие лишнего JOIN” для производительности и удобства фильтров.
Но тогда нужен механизм, который гарантирует консистентность (например, constraint, триггер, или правило в коде + тесты). В учебной системе можно держать workspace_id в tasks и контролировать его в приложении, а в идеале – усилить constraint’ами.
9.4. Схема данных: таблицы и ключевые поля
Ниже – списками, без таблиц (как вы просили). Названия – в стиле snake_case.
9.4.1. users
– id (uuid или bigserial; для современных систем uuid очень удобен)
– email (unique, not null)
– password_hash (not null)
– name (nullable)
– created_at (not null, default now())
– updated_at (not null, default now())
– deleted_at (nullable) – если хотите soft delete для пользователей (часто не надо; чаще “deactivated”)
Индексы:
– уникальный индекс на email
Constraints:
– email не пустой (можно простое CHECK (email <> ''))
– лучше хранить email в lower-case или нормализовать на входе
9.4.2. workspaces
– id
– name (not null)
– owner_user_id (not null, FK → users.id)
– created_at, updated_at
– deleted_at (nullable, если soft delete workspace)
Индексы:
– индекс на owner_user_id
Constraints:
– FK на owner
– CHECK (name <> '')
9.4.3. workspace_members
Это центральная таблица для прав.
– workspace_id (FK → workspaces.id)
– user_id (FK → users.id)
– role (not null; например: owner/admin/member)
– created_at
Ключи:
– составной PK (workspace_id, user_id) – так проще запретить дубль участия.
Constraints:
– role ограничить через CHECK (role IN ('owner','admin','member'))
или через PostgreSQL ENUM (ENUM удобен, но миграции могут быть чуть менее приятными; CHECK проще менять).
Индексы:
– индекс на user_id (быстрый поиск “где состоит пользователь”)
Практический нюанс:
– owner можно хранить и в workspaces.owner_user_id, и как запись membership с ролью owner.
Это полезно: membership даёт единый механизм прав, а поле owner – быстрый доступ и “один владелец”.
Тогда важно синхронизировать (обычно в коде + ограничениями).
9.4.4. projects
– id
– workspace_id (FK → workspaces.id)
– name
– description (nullable)
– status (active/archived)
– created_at, updated_at
– deleted_at (если soft delete проектов)
Индексы:
– (workspace_id, status) – список проектов по workspace
– возможно (workspace_id, name) если хотите искать по имени
Constraints:
– CHECK (status IN ('active','archived'))
9.4.5. tasks
– id
– workspace_id (FK → workspaces.id) – можно держать для ускорения фильтров
– project_id (FK → projects.id)
– title (not null)
– description (nullable)
– status (todo/in_progress/done/canceled)
– priority (low/medium/high, nullable)
– assignee_user_id (nullable, FK → users.id)
– reporter_user_id (not null, FK → users.id)
– due_date (nullable, date or timestamptz – зависит от требований)
– created_at, updated_at
– deleted_at (nullable; soft delete задач часто полезен)
Индексы (важное):
– (project_id, created_at desc, id desc) – для списков задач в проекте
– (workspace_id, status, created_at desc) – для фильтра по статусу
– (workspace_id, assignee_user_id, status) – “мои задачи”
– индекс на deleted_at не нужен, но частичный индекс полезен (см. ниже)
Partial index (практика):
– индекс только по “живым” задачам:
например индекс по (project_id, created_at desc) WHERE deleted_at IS NULL.
Это часто сильно помогает, потому что приложение почти всегда работает с живыми задачами.
Constraints:
– CHECK на статус и priority
– CHECK (title <> '')
Нюанс консистентности:
– если tasks.workspace_id хранится отдельно от projects.workspace_id, вы должны гарантировать, что они совпадают.
В простом варианте – правило в коде (при создании задачи берем workspace_id из проекта).
В строгом варианте – триггер или сложный constraint (в Postgres это решаемо, но для учебной книги можно оставить на уровне кода + тестов).
9.4.6. comments
– id
– task_id (FK → tasks.id)
– author_user_id (FK → users.id)
– body (not null)
– created_at, updated_at
– deleted_at (nullable)
Индексы:
– (task_id, created_at, id) – список комментариев к задаче
Constraints:
– CHECK (body <> '')
9.4.7. labels
– id
– workspace_id (FK → workspaces.id)
– name (not null)
– color (nullable)
– created_at, updated_at
– deleted_at (nullable)
Индексы:
– уникальность имени метки в workspace: unique (workspace_id, lower(name))
(обычно “Bug” и “bug” – одна и та же метка для людей)
9.4.8. task_labels
many-to-many связь:
– task_id (FK → tasks.id)
– label_id (FK → labels.id)
– created_at
Ключи:
– составной PK (task_id, label_id)
Индексы:
– индекс на label_id (чтобы быстро находить задачи по метке)
9.4.9. audit_events
– id
– workspace_id (nullable, FK → workspaces.id)
– actor_user_id (nullable, FK → users.id) – иногда события системные
– action (not null) – строка типа task.created
– entity_type (nullable) – task, project, …
– entity_id (nullable)
– ip (nullable)
– user_agent (nullable)
– metadata (jsonb, nullable)
– created_at
Индексы:
– (workspace_id, created_at desc)
– (actor_user_id, created_at desc)
– возможно (entity_type, entity_id)
9.4.10. idempotency_keys
Чтобы POST можно было безопасно повторять:
– id (uuid)
– user_id (FK → users.id)
– key (not null) – значение Idempotency-Key
– method (not null) – POST
– path (not null) – например /api/v1/projects/{id}/tasks
– request_hash (nullable) – чтобы ловить конфликт payload’ов
– response_code (not null)
– response_body (jsonb или text, not null)
– created_at
– expires_at
Индексы:
– unique (user_id, key, method, path)
– индекс на expires_at (чтобы чистить)
9.5. Индексы: как думать, чтобы не сделать “индекс на всё”
Индекс – это ускорение чтения ценой:
– места на диске,
– замедления записи,
– более сложного планирования.
9.5.1. Практическое правило: “индексируем то, чем фильтруем и сортируем”
Если ваш endpoint “список задач” почти всегда делает:
– WHERE workspace_id = ?
– AND status = ?
– ORDER BY created_at desc
То индекс должен соответствовать этой форме. Обычно порядок полей в индексе:
1) равенства (workspace_id, status)
2) сортировка (created_at, id)
9.5.2. “id в конце” для стабильной сортировки
Если вы делаете cursor pagination, вам нужна уникальная сортировка.
То есть сортируем по (created_at, id), а не только по created_at.
9.5.3. Partial indexes для soft delete
Если в таблице много удалённых записей, но вы почти всегда выбираете живые:
– делайте индекс с условием WHERE deleted_at IS NULL.
Это дешевле и эффективнее, чем индексировать всё подряд.
9.6. Constraints и foreign keys: когда база “воспитывает” приложение
Constraints – это ваша страховка от “случайно сохранили мусор”.
Что стоит делать всегда:
– NOT NULL там, где без значения сущность теряет смысл
– CHECK для ограниченных наборов значений (status, role)
– UNIQUE там, где нельзя дублировать (email, membership, label name)
– FOREIGN KEY почти везде, где есть ссылки
9.6.1. Foreign key и поведение при удалении
Варианты:
– ON DELETE RESTRICT – не даём удалить родителя, пока есть дети (часто лучший вариант)
– ON DELETE CASCADE – удаляем детей автоматически (опасно, но иногда удобно)
– ON DELETE SET NULL – “ссылка исчезла, но запись живёт”
Для TaskFlow здравый баланс:
– workspace → проекты: RESTRICT (или soft delete)
– project → tasks: RESTRICT/soft delete
– task → comments: RESTRICT/soft delete
– users в assignee: SET NULL (если пользователь деактивирован, задача остаётся, но без исполнителя)
9.6.2. Почему каскад иногда опасен
CASCADE легко превращается в кнопку “удалить всё случайно”.
Кто-то удалил workspace – исчезли проекты, задачи, комментарии, аудит (если вы не думали), и у вас остались только воспоминания.
9.7. Soft delete vs hard delete: как выбрать без философских войн
Hard delete – физическое удаление строки (DELETE FROM …).
Soft delete – отметка “удалено” (например deleted_at), но строка остаётся.
9.7.1. Когда soft delete – хороший выбор
– для задач и комментариев: полезно восстановление, аудит, разбор конфликтов
– для проектов: “архив” и “удаление” часто путают, лучше мягко
– для labels: чтобы не ломать историю и фильтры
9.7.2. Когда hard delete проще и честнее
– временные таблицы типа idempotency_keys (там вообще TTL)
– логины/сессии/refresh tokens (если вы их храните)
– технические данные, которые не нужны для истории
9.7.3. Цена soft delete
– каждый SELECT должен помнить deleted_at IS NULL
– индексы и запросы усложняются
– нужен механизм “чистки навсегда” (например, удалять soft-deleted старше N дней)
Практика: сделайте “живые данные” дефолтом:
– все основные запросы выбирают только deleted_at IS NULL
– отдельные admin-ручки могут показывать удалённые
9.8. Миграции: почему без них нельзя (и чем отличаются инструменты)
Миграции – это способ изменять схему базы управляемо, а не “я на проде руками поправлю”.
Есть несколько популярных инструментов. Мы сравним их по простым критериям: удобство, контроль, гибкость, и как они чувствуют себя в многоязычной книге.
9.8.1. Flyway
Подход:
– миграции – SQL-файлы с версиями (V1__init.sql, V2__add_tasks.sql)
Плюсы:
– очень простой ментально: SQL – значит SQL
– легко ревьюить
– хорошо подходит для PostgreSQL
Минусы:
– сложнее делать “условные” миграции, но чаще это и не нужно
Конец ознакомительного фрагмента.
Текст предоставлен ООО «Литрес».
Прочитайте эту книгу целиком, купив полную легальную версию на Литрес.
Безопасно оплатить книгу можно банковской картой Visa, MasterCard, Maestro, со счета мобильного телефона, с платежного терминала, в салоне МТС или Связной, через PayPal, WebMoney, Яндекс.Деньги, QIWI Кошелек, бонусными картами или другим удобным Вам способом.




