Читать книгу «Full stack Developer» онлайн полностью📖 — Юрий Белк — MyBook.

Глава 7. Нефункциональные требования

Нефункциональные требования – это то, из-за чего продукт либо выглядит профессионально, либо выглядит как “оно работало у меня на ноутбуке”.

В TaskFlow мы добавим набор требований, которые встречаются почти везде:

– rate limiting,

– идемпотентность (как паттерн для POST и “платёжных” операций),

– audit log,

– пагинация (cursor vs offset),

– SLA и наблюдаемость (observability).

Это всё звучит серьёзно, но в реальности это просто набор договорённостей и инструментов.

7.1. Rate limiting (ограничение частоты запросов)

Зачем нужно

Если вы открываете регистрацию и логин в интернет, то к вам придут:

– боты,

– перебор паролей,

– странные сканеры,

– и пользователи, которые “обновляют страницу каждые 200 мс”, потому что им тревожно.

Rate limiting решает две задачи:

1) Защищает инфраструктуру (CPU/DB/сеть).

2) Защищает пользователей (брутфорс на логин/пароль).

Что именно лимитируем

Минимум для TaskFlow:

– /auth/register – лимит на IP и/или на email

– /auth/login – лимит на IP и на аккаунт (email)

– “тяжёлые” списки (поиск задач) – лимит на пользователя

Как выражаем лимит

Удобно мыслить в терминах:

– N запросов за T секунд (например, 10 запросов/мин)

– отдельные лимиты для разных endpoint’ов

Пример политики (условно):

– регистрация: 5/час на IP

– логин: 10/10 минут на IP + 5/10 минут на email

– поиск задач: 60/мин на пользователя

Что возвращаем клиенту

Если лимит превышен, стандартно:

– HTTP 429 Too Many Requests

– заголовок Retry-After (сколько ждать)

И важный момент: сообщение в ответе не должно быть токсичным.

“Too many requests, retry later” – нормально. “Вы слишком много хотите” – это уже лишнее.

7.2. Idempotency для POST (и “платёжных” операций как паттерн)

Идемпотентность – это способность повторить запрос и получить тот же результат, не создав дубликатов.

Почему это важно:

– сеть ненадёжна,

– клиент может не получить ответ и повторить запрос,

– прокси может повторить запрос,

– пользователь может нажать “создать” дважды (классика жанра).

Где нужна идемпотентность

В TaskFlow критично для:

– POST /tasks (создание задач)

– POST /projects

– любые “операции эффекта” (например, отправка webhook/уведомления, загрузка файла – по дизайну)

Идея из платежей:

– клиент отправляет Idempotency-Key

– сервер по этому ключу понимает: это повтор или новый запрос

– повтор возвращает прежний результат

Как это выглядит

Клиент отправляет заголовок:

– Idempotency-Key: <random-uuid>

Сервер:

1) проверяет, есть ли запись по ключу + пользователю + endpoint’у

2) если есть – возвращает сохранённый результат (например, 201 и тело созданной задачи)

3) если нет – выполняет операцию, сохраняет результат и возвращает

Важные детали

– Ключ должен быть уникальным на сторону клиента (обычно UUID).

– Хранить идемпотентные ключи надо с TTL (например, 24 часа).

– Нужно привязывать ключ к пользователю (или workspace), иначе один пользователь сможет “повторить” чужую операцию.

Что сохранять

Минимально:

– статус-код,

– response body,

– время создания,

– fingerprint запроса (опционально).

И ещё правило: если по тому же ключу пришёл другой запрос (другая payload), можно вернуть 409 Conflict. Это честно и защищает от странных багов клиента.

7.3. Audit log (журнал аудита)

Аудит – это ответ на вопрос: кто и что сделал.

Даже если вы не банк, аудит полезен:

– для разборов инцидентов,

– для поддержки пользователей,

– для безопасности (“кто удалил проект?”),

– для аналитики.

Что писать в аудит

События минимум:

– user зарегистрировался

– user вошёл (логин)

– создан/обновлён/архивирован проект

– создана/обновлена/удалена задача

– добавлен/изменён/удалён комментарий

– изменения ролей и участников workspace

Структура записи (концептуально)

AuditEvent:

– id

– workspace_id (если событие в workspace)

– actor_user_id (кто сделал)

– action (например: task.created, project.archived)

– entity_type и entity_id

– timestamp

– metadata (JSON: старые/новые значения, ip, user-agent и т.п.)

Где хранить

Для учебного проекта:

– отдельная таблица в основной БД – нормально.

В больших системах:

– иногда события идут в отдельное хранилище/лог-систему.

Тонкий момент: PII

Если у вас есть персональные данные, не пишите в аудит лишнее.

Например, пароль и токены – никогда.

Email – можно, но лучше как идентификатор, не как “лог всего”.

7.4. Pagination: cursor vs offset (сравнение и выбор)

Списки задач, комментариев, проектов – это обязательно пагинация.

Есть два основных подхода:

– Offset pagination: limit=20&offset=40

– Cursor pagination: limit=20&cursor=…

Offset pagination

Плюсы:

– простая для понимания,

– легко прыгать на “страницу 5”.

Минусы:

– на больших данных может быть медленнее (offset заставляет БД “пропускать” строки),

– нестабильность при изменениях: если между запросами добавили записи, страница “поплывёт”.

Cursor pagination

Плюсы:

– стабильнее при добавлениях/удалениях,

– обычно лучше по производительности на больших объёмах,

– идеально подходит для “бесконечной ленты”.

Минусы:

– сложнее для клиентов,

– “страница 5” как концепция исчезает (есть только “вперёд/назад”, если реализовано).

Что выбираем для TaskFlow

Реалистичный выбор:

– для задач и комментариев: cursor pagination

– для простых справочников (labels, members) можно offset, но лучше быть последовательными

Cursor обычно строится на:

– (created_at, id) как “ключ сортировки”

– курсор кодируется в base64 и передаётся как строка

Пример сортировки:

– “новые сверху”: сортируем по created_at DESC, id DESC

– курсор хранит последнюю запись текущей страницы

7.5. SLA и Observability (наблюдаемость)

Что такое SLA в нашем контексте

SLA обычно формализуют в процентах доступности (“99.9%”). Для учебного проекта важнее другая мысль:

> Мы должны уметь доказать, что сервис жив, и быстро понять, если он болен.

Минимальные SLO (цели) для API:

– p95 latency для основных endpoint’ов, например < 300–500 мс (условно)

– error rate < 1%

– доступность (uptime) по health-check

Что нужно из observability

Минимальный набор, который стоит закладывать:

– структурированные логи (JSON, с request_id)

– метрики (количество запросов, 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 <token>

Где токен берётся:

– из 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:

– email

– password

– name (опционально)

Responses:

– 201 → создан пользователь (+ возможно токен)

– 400 validation_error

– 409 conflict (email занят)

– 429 rate_limited

POST /api/v1/auth/login

Логин.

Request:

– email

– 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.

Теперь можно переходить к реализации на любом языке, не споря “как лучше назвать поле” на каждом шаге – всё уже зафиксировано контрактом.

1
...