Модуль 3.4. API-дизайн: REST / GraphQL / gRPC
Темы: ресурсы, версии, контракт, пагинация, фильтры, ошибки, схемы (OpenAPI), безопасность (OAuth2/JWT), очереди и события. Артефакты: контракт API (OpenAPI). Практика: описать 5 эндпоинтов, схемы запрос/ответ.
Задача системного аналитика (SA) — превратить требования в читаемый, проверяемый и устойчивый контракт между командами и системами. Контракт API — это не только список урлов/полей, а правила эволюции, безопасность, надёжность (идемпотентность/ретраи), наблюдаемость и совместимость. На выходе модуля вы сможете спроектировать REST/GraphQL/gRPC API, увязать их с событиями/очередями, оформить OpenAPI и задать стандарты качества.
Как действовать SA (к исполнению)
- Смоделируйте ресурсы и связи (из ER/UC/state-машин).
- Выберите стиль (REST/GraphQL/gRPC) по сценариям (см. §2).
- Опишите контракт: схемы, ошибки, безопасность, пагинацию/фильтры, лимиты.
- Пропишите правила версионирования и депрекейта.
- Заложите идемпотентность/ретраи/корреляцию.
- Добавьте наблюдаемость (trace, метрики, логи) и rate limit.
- Утвердите в PR с contract-tests и openapi-diff как гейт.
Как выбрать: REST, GraphQL, gRPC (и где события)
|
Критерий |
REST |
GraphQL |
gRPC |
|---|---|---|---|
|
Потребитель |
веб/мобайл, B2B |
веб/мобайл со сложными экранами |
межсервисное, высоконагруженное |
|
Схема |
OpenAPI/JSON Schema |
SDL (схема GraphQL) |
proto3 |
|
Транспорт |
HTTP/1.1 |
HTTP |
HTTP/2 |
|
Паттерн |
ресурс-ориентированный |
клиент запрашивает «ровно нужные поля» |
RPC/стримы (uni/bi) |
|
Плюсы |
кэш, простота, понятные коды |
минимизирует «over/under-fetch», одна точка входа |
компактность, скорость, дедлайны, стриминги |
|
Минусы |
«раздутые» ответы, N+1 вызов |
N+1 в резолверах, кэш/авторизация сложнее |
требует тулчейн, бинарные пейлоады |
|
Где события/очереди |
факт (pub/sub), команды (очередь), outbox/CDC в дополнение к синхронному API |
Практический совет:
- Внешние интеграции/мобайл — REST.
- Сложные клиентские экраны/агрегации — GraphQL.
- Внутри микросервисов — gRPC.
- Факты домена для всех — события (AsyncAPI) + outbox.
Ресурсная модель (REST)
Принципы: существительные, коллекции во множественном числе, иерархия для подресурсов.
Примеры:
- /orders (GET список, POST создать)
- /orders/{id} (GET/PUT/PATCH/DELETE)
- /orders/{id}/items (коллекция)
-
Действия: избегаем RPC-глаголов в пути; используем подресурс-команда:
/payments/{id}:capture, /orders/{id}:cancel (или POST /payments/{id}/capture)
Полезные расширения:
- ?fields= (частичный ответ), ?include= (развёртка связей), ?sort=, ?filter[...].
Версионирование и совместимость
- SemVer для схем/контрактов: vMAJOR.MINOR.PATCH.
- Где хранить версию: URL /v1/... или заголовок X-Api-Version; для событий — в имени/схеме.
- Backward-compat: можно добавлять опциональные поля; нельзя менять смысл/тип, удалять без MAJOR.
- Deprecation policy: заголовки Deprecation, Sunset; поддержка N/N−1.
- openapi-diff — гейт: ломающее изменение → блокер релиза.
Пагинация / фильтры / сортировка
-
Пагинация:
- Offset/limit: ?offset=0&limit=50 (просто, но дорога на больших объёмах).
- Cursor: ?cursor=eyJpZCI6...&limit=50 (стабильно при вставках; возвращайте next_cursor).
- Сортировка: ?sort=-createdAt,amount (минус — убывание).
- Фильтры: предикаты в явной форме: ?status=PAID&amount_gte=100&createdAt_lte=....
GraphQL: connections (edges/pageInfo), курсоры; gRPC: поля page_size, page_token.
Ошибки и коды
REST (рекомендуемая модель ошибки — Problem+JSON):
{
"type": "https://errors.example.com/payment/insufficient-funds",
"title": "Insufficient funds",
"status": 402,
"code": "PAYMENT_002",
"detail": "Card declined by issuer",
"instance": "urn:err:...uuid...",
"correlationId": "b2f1-...",
"retryable": false
}
Коды: 200/201/202, 204; 400/401/403/404/409/422; 429; 5xx.
Возвращайте Retry-After для 429/503; всегда логируйте/отдавайте correlationId.
gRPC: статусы OK, INVALID_ARGUMENT, NOT_FOUND, ALREADY_EXISTS, PERMISSION_DENIED, UNAVAILABLE, DEADLINE_EXCEEDED...; детали ошибки — в google.rpc.Status.
GraphQL: errors[] с path, extensions.code; на уровне HTTP часто 200, но код — внутри ошибки.
Идемпотентность, ретраи, дедлайны
-
Идемпотентность для небезопасных операций (создание/изменение):
Idempotency-Key + хранение «вход→ответ» (TTL ≥ окно ретраев). - Ретраи: только на таймауты/5xx, экспонента+джиттер, максимум попыток.
- gRPC-deadlines: клиент обязан слать deadline; сервер уважает и завершает раньше.
Безопасность: OAuth2/JWT, MTLS
-
OAuth2:
- Client Credentials — сервис↔сервис, scope-ы по ресурсам.
- Authorization Code + PKCE — пользовательские клиенты.
- JWT: iss/aud/exp/nbf/sub/scope; алгоритм RS256/ES256, kid + JWK-ротация.
- Проверки: подпись, срок, audience, отзыв (introspection/short-lived + refresh).
- Роли/права: scopes (payments:write), атрибутная авторизация (ABAC).
- PII: маски в логах; минимизация полей.
- Webhooks: MTLS/подпись (HMAC-SHA256, заголовок-подпись), timestamp+nonce, окно anti-replay.
Кэширование и производительность
- ETag/If-None-Match, Cache-Control: public,max-age=60 для GET.
- Группируйте поля (projection/fields=), компрессия (gzip/br), пула соединений.
- Для «списков» — курсоры и индексы под сортировку/фильтры.
Наблюдаемость
- Tracing: traceparent/tracestate, X-Correlation-Id.
- Метрики: latency p95/p99, error-rate, RPS, 4xx/5xx, retries, rate limit hits.
- Логи: структурированные, обязательно записываем correlationId, userId/service, scope, error.code.
События и очереди рядом с API
- События («факты») — PaymentCaptured, OrderShipped (AsyncAPI, Avro/Proto), доставка at-least-once, дедуп по eventId.
- Outbox + CDC — чтобы не терять факты между БД и шиной.
- Версионирование событий: имя/схема payment-captured.v1; эволюция — только безопасные добавления.
Типовые анти-паттерны и риски
- RPC через REST: POST /createPayment — теряется кэш/семантика; делайте ресурс /payments.
- Ломающие изменения без MAJOR: клиенты падают. Включайте openapi-diff-гейт.
- Смешение доменов: один эндпоинт делает всё. Разделяйте ресурсы и команды.
- Нет идемпотентности: дубли при ретраях → деньги «умножаются».
- Секьюрити-дыры: долгоживущие JWT, отсутствие aud, нет проверки kid.
- GraphQL без лимитов: дорогостоящие запросы (нужны depth/complexity-лимиты, persisted queries).
- gRPC без дедлайнов: зависшие вызовы и утечки.
Артефакт: контракт API (OpenAPI 3.1 — укороченный пример, 5 эндпоинтов)
openapi: 3.1.0
info:
title: Commerce API
version: 1.0.0
servers:
- url: https://api.example.com/v1
security:
- oauth2CC: [payments:write, orders:read]
- bearerAuth: []
paths:
/orders:
get:
summary: List orders
parameters:
- in: query; name: cursor; schema: { type: string }
- in: query; name: limit; schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
- in: query; name: status; schema: { $ref: '#/components/schemas/OrderStatus' }
- in: query; name: sort; schema: { type: string, example: "-createdAt" }
- in: query; name: fields; schema: { type: string, example: "orderId,totalAmount,currency" }
responses:
'200':
description: OK
headers:
X-Next-Cursor: { schema: { type: string } }
content:
application/json:
schema:
type: object
properties:
items: { type: array, items: { $ref: '#/components/schemas/Order' } }
/orders/{orderId}:
get:
summary: Get order
parameters:
- in: path; name: orderId; required: true; schema: { type: string, format: uuid }
responses:
'200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Order' } } } }
'404': { $ref: '#/components/responses/NotFound' }
/payments:
post:
summary: Create payment (idempotent)
parameters:
- in: header; name: Idempotency-Key; required: true; schema: { type: string, minLength: 8 }
- in: header; name: X-Correlation-Id; required: true; schema: { type: string }
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/PaymentCreate' }
responses:
'201': { description: Created, headers: { Location: { schema: { type: string, format: uri } } },
content: { application/json: { schema: { $ref: '#/components/schemas/Payment' } } } }
'202': { description: Accepted (PSP slow) }
'409': { $ref: '#/components/responses/Conflict' }
'422': { $ref: '#/components/responses/Unprocessable' }
/payments/{paymentId}:
get:
summary: Get payment
parameters:
- in: path; name: paymentId; required: true; schema: { type: string, format: uuid }
responses:
'200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Payment' } } } }
'404': { $ref: '#/components/responses/NotFound' }
/refunds:
post:
summary: Create refund (idempotent)
parameters:
- in: header; name: Idempotency-Key; required: true; schema: { type: string }
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/RefundCreate' }
responses:
'201': { description: Created, content: { application/json: { schema: { $ref: '#/components/schemas/Refund' } } } }
'422': { $ref: '#/components/responses/Unprocessable' }
components:
securitySchemes:
oauth2CC:
type: oauth2
flows:
clientCredentials:
tokenUrl: https://auth.example.com/oauth/token
scopes:
payments:write: Create/capture/refund payments
orders:read: Read orders
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
responses:
NotFound:
description: Resource not found
content:
application/problem+json:
schema: { $ref: '#/components/schemas/Problem' }
Conflict:
description: Conflict / duplicate / wrong state
content:
application/problem+json:
schema: { $ref: '#/components/schemas/Problem' }
Unprocessable:
description: Validation error
content:
application/problem+json:
schema: { $ref: '#/components/schemas/Problem' }
schemas:
OrderStatus:
type: string
enum: [CREATED, PAID, SHIPPED, DELIVERED, CANCELLED]
Money:
type: object
required: [amount, currency]
properties:
amount: { type: string, pattern: "^[0-9]+(\\.[0-9]{2})?$" }
currency: { type: string, minLength: 3, maxLength: 3 }
Order:
type: object
required: [orderId, totalAmount, currency, status, createdAt]
properties:
orderId: { type: string, format: uuid }
status: { $ref: '#/components/schemas/OrderStatus' }
totalAmount: { type: string }
currency: { type: string }
createdAt: { type: string, format: date-time }
items:
type: array
items:
type: object
required: [productId, qty, price]
properties:
productId: { type: string, format: uuid }
qty: { type: integer, minimum: 1 }
price: { $ref: '#/components/schemas/Money' }
PaymentCreate:
type: object
required: [orderId, amount]
properties:
orderId: { type: string, format: uuid }
amount: { $ref: '#/components/schemas/Money' }
method: { type: string, enum: [CARD, APPLE_PAY, GOOGLE_PAY] }
Payment:
type: object
required: [paymentId, orderId, amount, status, createdAt]
properties:
paymentId: { type: string, format: uuid }
orderId: { type: string, format: uuid }
status: { type: string, enum: [AUTHORIZED, CAPTURED, FAILED] }
amount: { $ref: '#/components/schemas/Money' }
createdAt: { type: string, format: date-time }
capturedAt: { type: string, format: date-time, nullable: true }
RefundCreate:
type: object
required: [paymentId, amount]
properties:
paymentId: { type: string, format: uuid }
amount: { $ref: '#/components/schemas/Money' }
reason: { type: string, enum: [CUSTOMER_REQUEST, DUPLICATE, FRAUD_SUSPECTED] }
Refund:
type: object
required: [refundId, paymentId, amount, status, createdAt]
properties:
refundId: { type: string, format: uuid }
paymentId: { type: string, format: uuid }
amount: { $ref: '#/components/schemas/Money' }
status: { type: string, enum: [PENDING, COMPLETED, FAILED] }
createdAt: { type: string, format: date-time }
Problem:
type: object
required: [type, title, status, code, correlationId]
properties:
type: { type: string, format: uri }
title: { type: string }
status: { type: integer }
code: { type: string }
detail: { type: string }
instance: { type: string }
correlationId: { type: string }
retryable: { type: boolean }
В реальном проекте добавьте RateLimit-* хедеры, ETag/Cache-Control, примеры (example) и test-cases (JSON).
Сопутствующие контракты (кратко)
GraphQL (фрагмент SDL):
type Query {
order(id: ID!): Order
orders(after: String, first: Int, status: OrderStatus): OrderConnection!
}
type Mutation {
createPayment(input: PaymentInput!): Payment!
}
type OrderConnection { edges: [OrderEdge!]!, pageInfo: PageInfo! }
Практика: лимиты глубины/стоимости, persisted queries, авторизация на поле.
gRPC (proto3, фрагмент):
service PaymentService {
rpc CreatePayment(CreatePaymentRequest) returns (Payment) {}
rpc GetPayment(GetPaymentRequest) returns (Payment) {}
}
message CreatePaymentRequest { string order_id = 1; Money amount = 2; string idempotency_key = 3; }
message Money { string amount = 1; string currency = 2; }
Используйте deadlines, retry-политику в клиенте, статус-детали.
Событие (AsyncAPI/описательно):
- Topic: payments.captured.v1
- Key: orderId
- Payload: { eventId, occurredAt, paymentId, orderId, amount {…} }
- Инварианты: без PII; идемпотентность по eventId.
Практика: «5 эндпоинтов + схемы запрос/ответ» (60–90 мин)
- Выберите домен (e-commerce/банк/ERP).
- Спроектируйте 5 эндпоинтов (2 чтения, 2 изменения, 1 поиск/список с пагинацией).
- Опишите OpenAPI: схемы сущностей, ошибки, безопасность, фильтры/сортировку, курсоры.
- Добавьте идемпотентность и X-Correlation-Id в мутирующие эндпоинты.
- Подготовьте 3 «Problem+JSON» примера (404/409/422).
- Приложите чек-лист: N/N−1, openapi-diff, контракт-тесты.
Критерии зачёта
- Последовательная ресурсная модель, понятные статусы/ошибки.
- Пагинация курсором, сортировка/фильтры, частичные ответы (fields=).
- Безопасность: OAuth2/JWT, scopes.
- Идемпотентность/ретраи описаны.
- Артефакт OpenAPI валиден (проверен линтером).
Вопрос–Ответ
Q: Где хранить версию — в URL или заголовке?
A: Для публичного REST — проще /v1. Для внутренних/гейтвея подойдёт заголовок. Главное — политика N/N−1 и release-ноты.
Q: Когда 202 вместо 201?
A: Когда действие длительное/внешний партнёр медлит: приняли, запускаем процесс, финал — через событие/webhook/поллинг.
Q: Как принимать дубликаты POST?
A: Идемпотентный ключ + таблица ответов (TTL). На повтор верните тот же ответ (200/201) и Idempotent-Replay: true.
Q: Что лучше для мобильного клиента: REST или GraphQL?
A: Если экраны сложные и «разноформатные» — GraphQL с persisted queries. Иначе — REST быстрее в интеграции и кэшируется CDN.
Q: Как защитить webhook?
A: MTLS или HMAC-подпись, заголовки timestamp, signature; окно anti-replay; идемпотентность на приёме.
Q: Нужно ли отдавать детальные техошибки?
A: Во внешних API — нет (только коды/короткие детали); подробности — в логи с корреляцией.
Шпаргалка (коротко)
- Ресурсы → REST; сложные выборки → GraphQL; межсервисный высоконагруженный → gRPC.
- Версии и совместимость — жёсткая политика (SemVer, N/N−1, deprecations).
- Пагинация курсорами; сортировка/фильтры — явные.
- Ошибки — Problem+JSON; всегда correlationId.
- Идемпотентность ключом, ретраи — только на retryable.
- OAuth2/JWT + scopes, JWK-ротация, MTLS при необходимости.
- Трассировка/метрики/логи — обязательны.
- События рядом: outbox+CDC, at-least-once + дедуп.



