Перейти к основному содержимому

Практикум — проектирование контракта API

Разработчику Архитектору

Практикум, шаг 2 из 8. Контракт пишем до кода на Python и C#. См. сценарий, теорию проектирования API.

Перед реализацией прогоните в песочнице сценарии «Нехватка остатка» и «Идемпотентность» — они соответствуют строкам таблиц ниже.


Принципы контракта

  • Ресурсы — существительные во множественном числе: /products, /orders, /reservations.
  • Действия вне CRUD — подресурс или отдельный ресурс: POST /reservations, POST /orders/{id}/confirm.
  • Тело запроса и ответа — JSON, даты в ISO 8601 UTC (2026-05-27T10:15:00Z).
  • Ошибки — единый формат Problem Details (RFC 7807).
{
"type": "https://orderdesk.local/errors/insufficient-stock",
"title": "Insufficient stock",
"status": 409,
"detail": "Product prod_7 has 2 units, requested 5",
"instance": "/api/v1/reservations"
}

catalog-api (Python) — публичный и внутренний API

Базовый URL: http://localhost:8100

МетодПутьНазначениеУспехИдемпотентность
GET/api/v1/productsСписок товаров (пагинация ?page=1&pageSize=20)200да
POST/api/v1/productsСоздать товар201нет
GET/api/v1/products/{productId}Карточка товара200 / 404да
PATCH/api/v1/products/{productId}Частичное обновление (имя, цена)200да
POST/api/v1/reservationsЗарезервировать остаток201 / 409нет*
DELETE/api/v1/reservations/{reservationId}Отменить резерв204да

* Повтор POST /reservations с тем же заголовком Idempotency-Key возвращает тот же 201 и тело — см. шаг 6.

Заголовки (catalog):

ЗаголовокКто шлётЗачем
Authorization: Bearer <jwt>Клиент (Postman)Доступ к CRUD товаров
X-Api-Keyorders-apiМежсервисные POST/DELETE резерва
Idempotency-Keyorders-apiБезопасный повтор резерва
X-Request-IdлюбойТрассировка в логах

orders-api (C#) — API для клиентов

Базовый URL: http://localhost:5200

МетодПутьНазначениеУспех
POST/api/v1/auth/tokenВыдать JWT (учебный login/password)200
GET/api/v1/ordersСписок заказов текущего пользователя200
POST/api/v1/ordersСоздать заказ и зарезервировать в каталоге201 / 502
GET/api/v1/orders/{orderId}Детали заказа200 / 404
POST/api/v1/orders/{orderId}/confirmПодтвердить после оплаты200
POST/api/v1/orders/{orderId}/cancelОтменить, снять резерв в каталоге200

WebSocket: ws://localhost:5200/ws/orders?access_token=<jwt>


Тела запросов и ответов (ключевые)

Создание товара

POST /api/v1/products

{
"sku": "WB-42",
"name": "Wireless mouse",
"price": 29.99,
"stockAvailable": 100
}

Ответ 201:

{
"id": "prod_a1b2",
"sku": "WB-42",
"name": "Wireless mouse",
"price": 29.99,
"stockAvailable": 100,
"createdAt": "2026-05-27T10:00:00Z"
}

Резервирование (вызов из orders-api)

POST /api/v1/reservations

{
"productId": "prod_a1b2",
"quantity": 3,
"orderRef": "ord_x9y8"
}

Ответ 201:

{
"reservationId": "res_m3n4",
"productId": "prod_a1b2",
"quantity": 3,
"expiresAt": "2026-05-27T11:00:00Z"
}

Создание заказа

POST /api/v1/orders

{
"lines": [
{ "productId": "prod_a1b2", "quantity": 3 }
]
}

Ответ 201:

{
"id": "ord_x9y8",
"status": "reserved",
"lines": [
{
"productId": "prod_a1b2",
"quantity": 3,
"unitPrice": 29.99,
"reservationId": "res_m3n4"
}
],
"total": 89.97,
"createdAt": "2026-05-27T10:05:00Z"
}

Фрагмент OpenAPI 3.1 (catalog)

Сохраните как catalog-api/openapi.yaml — источник правды для Postman и ревью:

openapi: 3.1.0
info:
title: OrderDesk Catalog API
version: 1.0.0
servers:
- url: http://localhost:8100
paths:
/api/v1/products:
get:
operationId: listProducts
parameters:
- name: page
in: query
schema: { type: integer, minimum: 1, default: 1 }
- name: pageSize
in: query
schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
responses:
"200":
description: OK
post:
operationId: createProduct
security: [{ bearerAuth: [] }]
responses:
"201":
description: Created
/api/v1/reservations:
post:
operationId: createReservation
security: [{ apiKeyAuth: [] }]
parameters:
- name: Idempotency-Key
in: header
required: true
schema: { type: string, maxLength: 64 }
responses:
"201":
description: Reserved
"409":
description: Insufficient stock
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
apiKeyAuth:
type: apiKey
in: header
name: X-Api-Key

Фрагмент OpenAPI (orders-api)

paths:
/api/v1/auth/token:
post:
operationId: issueToken
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [username, password]
properties:
username: { type: string }
password: { type: string, format: password }
responses:
"200":
description: JWT выдан
/api/v1/orders:
post:
operationId: createOrder
security: [{ bearerAuth: [] }]
responses:
"201": { description: Created }
"409": { description: Stock conflict }
"502": { description: Catalog unreachable }
/api/v1/orders/{orderId}/confirm:
post:
operationId: confirmOrder
security: [{ bearerAuth: [] }]

Полные спецификации храните рядом с кодом (catalog-api/openapi.yaml, orders-api/openapi.yaml). В Postman импортируйте оба файла через Import → OpenAPI — коллекция и примеры тел подтянутся автоматически.


Пагинация и метаданные списков

GET /api/v1/products и GET /api/v1/orders возвращают обёртку (рекомендуется для роста данных):

{
"items": [ { "id": "prod_a1b2", "name": "…" } ],
"page": 1,
"pageSize": 20,
"total": 134
}

Заголовок Link с rel="next" — альтернатива query-параметрам; в практикуме достаточно ?page= и ?pageSize=.


Матрица ответственности по кодам HTTP

КодКогда
400Невалидное тело (Pydantic / FluentValidation)
401Нет или просрочен JWT
403JWT есть, прав недостаточно
404Ресурс не найден
409Конфликт остатка при резерве
422Семантическая ошибка (пустой заказ)
502orders-api не достучался до catalog-api
503Сервис временно недоступен (circuit breaker)

Следующий шаг

Контракт задаёт форму JSON. Внутри каждого сервиса нужны свои сущности и DTO — Модели данных и маппинг.

См. также

Другие статьи этого же раздела в боковом меню (как на странице "О разделе").