Все уроки
Основы вебаСредний14 мин чтенияВ моём стеке

REST API

Проектируйте API, которые предсказуемы, безопасны для повторов и приятны в интеграции.

  • Resources
  • Status codes
  • Pagination
  • Versioning
  • Idempotency
Шаг 1/6
API — как официант. GET /menu — «принесите меню» — 200 OK, вот, пожалуйста.

За 30 секунд

REST API представляет ваши данные как ресурсы по URL, которые клиенты читают и изменяют стандартными HTTP-методами. Хорошие API предсказуемы: единообразные имена, правильные статус-коды, понятные ошибки, пагинация, версионирование и документация. Если всё сделано хорошо, веб-, мобильные и партнёрские приложения интегрируются быстро и безопасно.

Почему я это использую

  • Я проектирую и делаю REST API, которые обслуживают сайты, админки и мобильные приложения с одного бэкенда.
  • Предсказуемый API экономит больше времени, чем любой фреймворк: фронтенд- и мобильные разработчики перестают гадать.
  • Платежам и заказам нужны идемпотентность и понятные ошибки; я встраивал и то и другое в продакшен-API.
  • Партнёры интегрируются быстрее, когда API версионирован, имеет честные rate limit'ы и документирован в OpenAPI.

Как это работает

01Ресурсы и URL

REST (Representational State Transfer) моделирует предметную область как ресурсы — существительные вроде products, orders и customers. У каждого ресурса есть URL, а метод говорит, что с ним сделать. Используйте существительные во множественном числе и ID: /orders, /orders/1001, /orders/1001/items. Избегайте глаголов в путях вроде /getOrders или /createOrder — это уже говорит метод. Держите вложенность неглубокой (обычно хватает одного уровня). Действия, которые плохо ложатся на CRUD, моделируйте как подресурсы или изменения состояния: POST /orders/1001/cancellation или PATCH /orders/1001 с полем status. Используйте единообразное именование (camelCase или snake_case — выберите одно), даты ISO 8601 в UTC и ID, которые не выдают объёмы бизнеса, когда это важно. Аналогия: ресурсы — комнаты в здании, URL — их адреса, а методы — то, что вы в комнате делаете.

http
GET    /v1/orders?status=pending&page=2   # list
POST   /v1/orders                         # create
GET    /v1/orders/1001                    # read one
PATCH  /v1/orders/1001                    # partial update
DELETE /v1/orders/1001                    # delete
GET    /v1/customers/77/orders            # related collection
POST   /v1/orders/1001/refunds            # action as sub-resource

02Методы, статус-коды и формат ошибок

Сопоставляйте операции с методами, а результаты — со статус-кодами. Создание возвращает 201 Created с заголовком Location и новым ресурсом. Успех без содержимого — 204. Ошибки валидации — 422 (или 400 для некорректного JSON) с деталями по полям. Не аутентифицирован — 401, нет прав — 403, не найдено — 404, конфликт версии или состояния — 409, слишком много запросов — 429, баги сервера — 500. Выберите один формат ошибок и используйте его везде. RFC 9457 Problem Details (application/problem+json) — хороший стандарт: type, title, status, detail плюс ваши собственные поля, например ошибки по полям и request ID для поддержки. Никогда не отдавайте в продакшен-ошибках стектрейсы или SQL.

http
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json
 
{
  "type": "https://api.example.com/problems/validation",
  "title": "The request has invalid fields.",
  "status": 422,
  "errors": { "quantity": ["Must be at least 1."] },
  "requestId": "req_7f3a9c"
}

03Пагинация, фильтрация, сортировка

Никогда не возвращайте неограниченные списки. Offset-пагинация (?page=3&per_page=20) проста и позволяет перейти на любую страницу, но на больших таблицах становится медленной и может пропускать или повторять элементы, когда данные меняются. Курсорная пагинация (?cursor=eyJpZCI6MTAwMH0&limit=20) использует непрозрачный указатель, например последний увиденный ID и created_at; она быстрая и стабильная для лент, бесконечной прокрутки и синхронизаций, но не позволяет прыгнуть на 50-ю страницу. Возвращайте метаданные или ссылки, чтобы клиенты не собирали URL сами. Для фильтрации используйте понятные query-параметры (?status=paid&created_from=2026-01-01), для сортировки — ?sort=-created_at,total, для поиска — ?q=. Ограничивайте максимальный размер страницы и разрешайте фильтрацию и сортировку только по индексированным полям.

http
GET /v1/orders?status=paid&sort=-created_at&limit=20&cursor=eyJpZCI6MTAyMH0
 
{
  "data": [ { "id": 1019, "total": 12500, "status": "paid" } ],
  "meta": { "limit": 20, "hasMore": true },
  "links": { "next": "/v1/orders?status=paid&sort=-created_at&limit=20&cursor=eyJpZCI6MTAwMH0" }
}

04Версионирование и безопасное развитие

Клиенты, особенно мобильные приложения, которые пользователи не обновляют, зависят от вашего API годами. Добавляющие изменения безопасны: новые эндпоинты, новые необязательные поля, новые значения enum, если клиентам сказали терпимо относиться к неизвестным. Ломающие изменения — удаление или переименование полей, смена типов или смысла, превращение необязательного входного параметра в обязательный. Для ломающих изменений вводите новую версию. Версионирование в URL (/v1, /v2) самое заметное, и его проще всего маршрутизировать, кэшировать и отлаживать; версионирование через заголовок или media type сохраняет URL чистыми, но его сложнее тестировать. Некоторые API используют версии-даты в заголовке. Что бы вы ни выбрали, объявляйте об устаревании с датами, отправляйте заголовки Deprecation и Sunset и отслеживайте, какие клиенты всё ещё вызывают старые версии, прежде чем их отключать. Дополните это механизмом принудительного обновления в мобильных приложениях.

05Аутентификация, идемпотентность и rate limit'ы

Выбирайте способ аутентификации по типу клиента. Браузерные приложения на том же сайте: защищённые HttpOnly-cookie сессии плюс защита от CSRF. Мобильные и SPA-клиенты: короткоживущие access-токены (часто JWT) с refresh-токенами, через потоки OAuth 2 с PKCE для входа через сторонние сервисы. Партнёры «сервер–сервер»: API-ключи с ограниченными правами или OAuth client credentials, передаваемые в заголовках, никогда в URL. Сеть сбоит, поэтому делайте запись безопасной для повторов: клиенты отправляют заголовок Idempotency-Key с уникальным значением; сервер сохраняет ключ вместе с результатом и при повторе ключа возвращает тот же ответ, вместо того чтобы списать деньги или создать заказ дважды. Защищайте API rate limit'ами по ключу, пользователю или IP, отвечайте 429 с Retry-After и показывайте лимиты в заголовках, чтобы клиенты могли сбавить темп.

http
POST /v1/payments HTTP/1.1
Authorization: Bearer <access_token>
Idempotency-Key: 6f1c2d4e-8a7b-4c3d-9e2f-1a2b3c4d5e6f
Content-Type: application/json
 
{"orderId": 1001, "amount": 12500, "currency": "AMD"}
 
HTTP/1.1 429 Too Many Requests
Retry-After: 30
RateLimit-Policy: "default";q=100;w=60

06Документация в OpenAPI

OpenAPI (актуальная версия 3.1 согласована с JSON Schema) описывает ваши эндпоинты, параметры, тела запросов и ответов, ошибки и аутентификацию в файле YAML или JSON. Из этого единого источника вы получаете интерактивную документацию (Swagger UI, Redoc, Scalar), сгенерированные типизированные клиенты для TypeScript, Kotlin или Swift, mock-серверы и контрактные тесты. Генерируйте её из аннотаций или декораторов в коде (в NestJS есть встроенная поддержка, для Laravel — пакеты вроде Scramble), чтобы она никогда не расходилась с реальностью. Добавляйте примеры для каждого запроса и ошибки, описывайте пагинацию и rate limit'ы, публикуйте changelog. Хорошая документация — это фича: она сокращает время интеграции с недель до дней.

yaml
paths:
  /v1/orders/{id}:
    get:
      summary: Get one order
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        "200":
          description: The order
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Order" }
        "404": { $ref: "#/components/responses/NotFound" }

07REST, GraphQL, gRPC, WebSockets и вебхуки

Они решают разные задачи и часто живут бок о бок. REST — вариант по умолчанию для публичных и мобильных API: простой, кэшируемый, понятный любому инструменту. GraphQL позволяет клиентам запрашивать ровно те поля, которые нужны, с одного эндпоинта; это помогает, когда множество разных экранов собирают данные из многих источников, но цена — сложное кэширование, авторизация на уровне полей и лимиты стоимости запросов. gRPC использует Protocol Buffers поверх HTTP/2 со сгенерированными клиентами и стримингом; отлично подходит для быстрых типизированных вызовов между сервисами, неудобен в браузерах. WebSockets держат открытым двустороннее соединение для чатов, статуса заказа в реальном времени и дашбордов. Вебхуки — это «обратные API»: ваш сервер вызывает URL партнёра, когда что-то происходит, например платёж прошёл; подписывайте их HMAC, повторяйте с backoff и делайте получателей идемпотентными.

  • Публичный, партнёрский или мобильный API: REST + OpenAPI.
  • Много клиентов с очень разными потребностями в данных: рассмотрите GraphQL.
  • Внутренние высоконагруженные сервисы: gRPC.
  • Обновления пользователям в реальном времени: WebSockets или SSE. События для других систем: вебхуки.

Когда применять

  • Публичные, партнёрские и мобильные API, которые должно быть легко понять и интегрировать.
  • Бэк-офисы и интернет-магазины с большим количеством CRUD.
  • Когда важны HTTP-кэширование, CDN и стандартные инструменты.
  • Как стабильный контракт между бэкендом и несколькими фронтендами.

Когда не стоит

  • Очень «болтливые» внутренние вызовы между сервисами, где важнее задержка и строгие схемы: рассмотрите gRPC.
  • Push клиентам в реальном времени: используйте WebSockets, SSE или push-уведомления вместо поллинга.
  • Экраны, которым в одном запросе нужно много вложенных ресурсов из многих источников: GraphQL или отдельный агрегирующий эндпоинт могут оказаться проще.

Альтернативы

  • GraphQL

    Клиент сам выбирает поля через один эндпоинт; гибко, но кэширование и авторизация сложнее.

  • gRPC

    Типизированный быстрый RPC поверх HTTP/2 для внутренних сервисов.

  • tRPC

    Сквозные TypeScript-типы между Node-бэкендом и TS-фронтендом без файла схемы.

  • WebSockets / вебхуки

    Push-коммуникация для обновлений в реальном времени и событий между системами.

Лучшие практики

  1. Используйте существительные во множественном числе, единообразное именование и правильные статус-коды везде.
  2. Возвращайте один формат ошибок (Problem Details) с ошибками по полям и request ID.
  3. Пагинируйте каждый список, ограничивайте размер страницы и фильтруйте только по индексированным полям.
  4. Версионируйте с первого дня и объявляйте об устаревании с датами.
  5. Требуйте Idempotency-Key для платежей и создания заказов; автоматически повторяйте только идемпотентные вызовы.
  6. Ограничивайте частоту запросов по ключу или пользователю и возвращайте 429 с Retry-After.
  7. Генерируйте документацию OpenAPI из кода и публикуйте примеры и changelog.

Частые ошибки

  • Возвращать 200 на каждый ответ и прятать ошибки в теле.
  • Выставлять таблицы базы данных один к одному, раскрывая внутренние поля и привязывая клиентов к схеме.
  • Ломать мобильные клиенты, переименовывая поля без новой версии.
  • Эндпоинты списков без ограничений, которые начинают падать по таймауту по мере роста данных.
  • Проверять аутентификацию, но не авторизацию, из-за чего пользователи могут читать чужие ресурсы, просто поменяв ID.

Глубже

08Авторизация — риск номер один для API

OWASP API Security Top 10 (2023) ставит на первое место Broken Object Level Authorization: API проверяет, что пользователь залогинен, но не проверяет, что заказ, счёт или файл, который он запрашивает, принадлежит ему, поэтому замена /orders/1001 на /orders/1002 раскрывает чужие данные. Сразу за ним идут broken function-level authorization (обычный пользователь вызывает админский эндпоинт) и broken object property level authorization (mass assignment, когда клиент выставляет role или price в теле PATCH). Надёжный паттерн — ограничивать каждый запрос вызывающим, а не проверять постфактум: загружайте данные через связь с пользователем или tenant, чтобы чужой ID просто ничего не находил и возвращал 404. Задавайте whitelist записываемых полей для каждого эндпоинта и используйте policy или guard, которые срабатывают на каждом маршруте. Случайные ID или ID с префиксом усложняют угадывание, но никогда не заменяют авторизацию.

php
// Laravel: scope by owner, then authorize with a policy
public function show(Request $request, int $id)
{
    $order = $request->user()->orders()->findOrFail($id); // 404 for others
    $this->authorize('view', $order);
    return new OrderResource($order);
}
 
// Only allow known fields to be written
$data = $request->validate([
    'note'     => ['nullable', 'string', 'max:500'],
    'quantity' => ['required', 'integer', 'min:1'],
]);

09Долгие операции и надёжные события

Некоторые запросы не успевают завершиться за обычный таймаут: экспорты, импорты, обработка видео, массовые обновления. Вместо того чтобы держать соединение открытым, примите работу и верните 202 Accepted с заголовком Location, указывающим на ресурс статуса, например /v1/jobs/77. Клиент опрашивает этот ресурс или подписывается на вебхук и в конце переходит по ссылке на результат. Смежная проблема — надёжная публикация событий. Если код сохраняет заказ, а затем отправляет вебхук, падение между этими шагами теряет событие; если сначала отправить, а потом не сохранить, получится событие-призрак. Это решает паттерн transactional outbox: записывайте заказ и строку outbox в одной транзакции базы, а воркер доставляет строки outbox с повторами. Потребители всё равно должны дедуплицировать по ID события, потому что доставка — at least once.

http
POST /v1/exports HTTP/1.1
Content-Type: application/json
 
{"type": "orders", "from": "2026-10-01", "to": "2026-10-31"}
 
HTTP/1.1 202 Accepted
Location: /v1/jobs/77
 
GET /v1/jobs/77  ->  {"status": "running", "progress": 40}
GET /v1/jobs/77  ->  {"status": "done", "resultUrl": "/v1/exports/77/file"}

10Алгоритмы rate limiting

Fixed window считает запросы за календарную минуту; это просто, но на границе допускает двойные всплески (100 в 12:00:59 и 100 в 12:01:00). Sliding window учитывает с весом счётчик предыдущего окна, чтобы это сгладить. Token bucket даёт каждому клиенту «ведро», которое пополняется с постоянной скоростью до максимума, поэтому короткие всплески разрешены, а долгосрочная скорость остаётся фиксированной; это соответствует поведению реальных клиентов. Храните счётчики в Redis с атомарными операциями, чтобы их разделяли все серверы приложения. Ключи выбирайте аккуратно: по API-ключу для партнёров, по пользователю для залогиненного трафика, по IP — только в крайнем случае, ведь за NAT или мобильным оператором один IP могут делить многие пользователи. Возвращайте 429 с Retry-After и задавайте отдельные, более строгие лимиты для дорогих или чувствительных эндпоинтов вроде поиска, логина и экспортов.

php
// Laravel: named limiters, applied with ->middleware('throttle:api')
RateLimiter::for('api', function (Request $request) {
    return $request->user()
        ? Limit::perMinute(120)->by('user:'.$request->user()->id)
        : Limit::perMinute(30)->by('ip:'.$request->ip());
});
 
RateLimiter::for('login', fn (Request $request) =>
    Limit::perMinute(5)->by($request->input('email').'|'.$request->ip())
);

11Производительность: форма payload, N+1 и кэширование

Большинство медленных API медленны в базе данных. Эндпоинт списка, который загружает 50 заказов, а затем для каждого запрашивает клиента, делает 51 запрос: это проблема N+1. Используйте eager loading для связей, выбирайте только нужные колонки и добавляйте индексы под фильтры и сортировку; в разработке логируйте число запросов на каждый HTTP-запрос. Формируйте payload под клиента: sparse fieldsets (?fields=id,total,status) или параметры include (?include=customer) позволяют одному эндпоинту обслуживать и компактный список, и детальный просмотр. Сжимайте JSON через gzip или Brotli. Для ресурсов, которые часто читают, отправляйте ETag или Last-Modified, чтобы клиенты получали ответы 304, и кэшируйте вычисленные ответы в Redis с явной инвалидацией при записи. Публичные неперсональные эндпоинты, например каталог товаров, можно кэшировать в CDN через s-maxage. Измеряйте p95 latency по каждому эндпоинту, а не средние значения.

Реальные ситуации

01

Пользователь может читать счета других клиентов

Проблема

Исследователь безопасности сообщает, что если поменять ID счёта в GET /v1/invoices/{id}, возвращаются PDF других клиентов. Эндпоинт требует валидный токен.

Как решить

Это broken object level authorization. Сначала исправьте эндпоинт: загружайте счёт через аккаунт вызывающего, например через связь invoices у пользователя или tenant, и возвращайте 404, если он не найден. Затем найдите в кодовой базе все маршруты, принимающие ID, и примените тот же паттерн или проверку policy, включая вложенные ресурсы и скачивание файлов. Добавьте автотесты, которые вызывают каждый эндпоинт с токеном второго пользователя и ожидают 404. Проверьте access-логи на последовательный перебор ID, чтобы понять, утекли ли данные, и действуйте по вашему процессу реагирования на инциденты и раскрытия.

02

В нестабильных сетях с клиентов списывают деньги дважды

Проблема

Поддержка видит двойные платежи от мобильных пользователей. В логах — два запроса POST /payments с разницей в несколько секунд и одинаковыми телами после таймаута на клиенте.

Как решить

Сделайте эндпоинт идемпотентным. Мобильный клиент генерирует UUID на каждую попытку оплаты и отправляет его как Idempotency-Key, переиспользуя при повторах. Сервер хранит ключ, хэш тела и ответ в таблице с уникальным ограничением на пару клиент–ключ. Повтор с тем же телом возвращает сохранённый ответ; другое тело с тем же ключом — 422; повтор, пока первый запрос ещё выполняется, — 409. Также передавайте ключ платёжному провайдеру, если он поддерживает идемпотентность, чтобы были защищены оба уровня.

03

Старая версия мобильного приложения ломается после релиза

Проблема

После переименования поля total в totalAmount в ответе заказов пользователи версии приложения прошлого года видят пустые суммы и не могут оформить заказ.

Как решить

Немедленно верните старое поле и пока отправляйте оба, ведь добавление поля не ломает совместимость. Введите правило: удаление и переименование полей — только в новой версии API или после периода deprecation. Логируйте версию приложения из заголовка каждого запроса, чтобы до удаления чего-либо знать, кто ещё пользуется старыми полями. Добавьте контрактные тесты, сгенерированные из спецификации OpenAPI, которые падают, если из ответа пропадает задокументированное поле. Для действительно неподдерживаемых старых версий используйте экран принудительного обновления, а не ломайте их молча.

Шпаргалка

Создание
POST /v1/orders -> 201 Created + Location: /v1/orders/1001
Частичное обновление с проверкой конкурентности
PATCH /v1/orders/1001 + If-Match: "v7" -> 200 or 412
Принято в фоновую обработку
202 Accepted + Location: /v1/jobs/77
Формат ошибки валидации
Content-Type: application/problem+json (RFC 9457) with type, title, status, errors
Курсорная пагинация
GET /v1/orders?limit=20&cursor=eyJpZCI6MTAwMH0
Sparse fieldsets и include
GET /v1/orders?fields=id,total,status&include=customer
Безопасные повторы для записи
Idempotency-Key: 6f1c2d4e-8a7b-4c3d-9e2f-1a2b3c4d5e6f
Превышен rate limit
429 Too Many Requests + Retry-After: 30
Сигналы deprecation
Deprecation: @1798761600 + Sunset: Fri, 01 Jan 2027 00:00:00 GMT
Проверить файл OpenAPI линтером
npx @redocly/cli lint openapi.yaml

В моих реальных проектах

  • Платформа заказов для ресторанов работает на одном REST API на Laravel с отдельными веб- и мобильным каналами и контроллерами, разделёнными на слои Shared, Ui, Mobile и Admin, — всего около 330 эндпоинтов.
  • В мультитенантном POS SaaS (NestJS, OpenAPI 3.1) я работал над версионированным гостевым API для мобильных приложений и публичным партнёрским API с API-ключами ограниченных прав, rate limit'ами и ротацией ключей.
  • Эта платформа использует ключи идемпотентности для партнёрских заказов, подписанные HMAC вебхуки, доставляемые через очередь, и возвращает 404 для строк другого тенанта.
  • В приложении для зоомагазина HTTP-клиент повторяет только идемпотентные методы, поэтому сетевые ошибки никогда не дублируют заказы.

Вопросы и ответы

Q1Что делает API RESTful?

REST — это архитектурный стиль: ресурсы, идентифицируемые по URL, управляемые через единый интерфейс (стандартные HTTP-методы), запросы без состояния, несущие весь нужный контекст, кэшируемые ответы и представления вроде JSON. На практике хороший REST API использует существительные для ресурсов, метод для выражения действия, правильные статус-коды, стандартные заголовки для кэширования и аутентификации и единообразный формат ошибок. Строгий REST также включает гипермедиа-ссылки, которые ведут клиента, но мало какие API реализуют это полностью. Я делаю упор на предсказуемость и правильную семантику HTTP, а не на чистоту, потому что именно это ускоряет интеграцию.

Q2Как вы делаете пагинацию для больших наборов данных?

Для админских таблиц, где пользователи прыгают между страницами, хватает offset-пагинации с сортировкой по индексу и ограниченным размером страницы. Для больших или быстро меняющихся данных — лент, бесконечной прокрутки и эндпоинтов синхронизации — я использую курсорную пагинацию: курсор кодирует ключ сортировки и ID последнего элемента, а запрос использует WHERE по этим колонкам, что остаётся быстрым на любой глубине и никогда не пропускает и не дублирует строки при появлении новых данных. Я возвращаю следующий курсор и флаг hasMore или ссылки, чтобы клиенты никогда не собирали URL сами. Общее количество на больших таблицах может быть дорогим, поэтому я делаю его необязательным.

Q3Что такое ключ идемпотентности и как его реализовать?

Это уникальное значение, которое клиент генерирует для одной логической операции, например платежа, и отправляет в заголовке Idempotency-Key. Сервер сохраняет ключ вместе с отпечатком запроса и ответом в рамках клиента — в таблице с уникальным ограничением или в Redis с TTL. Если тот же ключ приходит снова, сервер возвращает сохранённый ответ, а не обрабатывает запрос дважды; если тот же ключ приходит с другим телом, возвращается 422 или 409. Пока первый запрос ещё выполняется, дубликат получает 409 и повторяет попытку позже. Так POST становится безопасным для повторов после таймаутов.

Q4Как версионировать API и когда нужна новая версия?

Новая версия нужна только для ломающих изменений: удаления или переименования полей, смены типов или смысла, добавления обязательных входных параметров. Добавляющие изменения остаются в текущей версии, а клиентам говорят игнорировать неизвестные поля. Я предпочитаю версионирование в URL вроде /v1, потому что оно явное и его легко маршрутизировать, логировать и кэшировать. Когда выходит новая версия, старая получает уведомление об устаревании с датой отключения, заголовки Deprecation и Sunset и мониторинг тех, кто её ещё вызывает. Для мобильных приложений, которые пользователи обновляют не всегда, я держу старые версии дольше и использую механизм принудительного обновления для действительно неподдерживаемых релизов.

Q5Когда вы выберете GraphQL или gRPC вместо REST?

GraphQL подходит, когда многим клиентам нужны данные разной формы, собранные из многих источников, — например, в большом продукте с веб-, мобильными и партнёрскими фронтендами, где избыточная выборка или множество круговых путей становятся реальной проблемой. Цена — более сложное HTTP-кэширование, авторизация на уровне полей и лимиты сложности запросов. gRPC подходит для внутренних вызовов между сервисами, где важны низкая задержка, строгие типизированные контракты и стриминг, а браузеры не основной клиент. REST остаётся моим выбором по умолчанию для публичных, партнёрских и мобильных API, потому что его понимает любой инструмент, CDN и разработчик. Часто система их сочетает: REST снаружи, gRPC или очереди внутри.

Q6Как защитить вебхуки, которые вы отправляете или принимаете?

При отправке я подписываю каждый payload через HMAC-SHA256 секретом конкретного подписчика и включаю timestamp в подписываемое содержимое и заголовки. Доставка идёт через очередь с повторами и экспоненциальным backoff, а у каждого события есть уникальный ID. При приёме я проверяю подпись сравнением за постоянное время, отклоняю старые timestamp'ы, чтобы блокировать повторное воспроизведение, и обрабатываю идемпотентно, сохраняя ID событий, потому что провайдеры доставляют «как минимум один раз» и могут прислать повторно. Я быстро отвечаю 2xx, а тяжёлую работу делаю в фоновой задаче. И наконец, в вопросах денег я никогда слепо не доверяю payload: критическое состояние подтверждаю через API провайдера.

Вопросы уровня senior

S1Как бы вы спроектировали публичный API для сторонних партнёров?

Я начинаю с контракта: спецификация OpenAPI 3.1, версионирование в URL, ошибки в формате Problem Details, курсорная пагинация и даты в ISO, и всё это проходит ревью до реализации. Аутентификация — через API-ключи со scope или OAuth client credentials; ключи хранятся в виде хэшей, показываются один раз, их можно ротировать и отзывать. Каждый запрос ограничен tenant партнёра, а rate limit применяется по ключу с заголовками, показывающими оставшуюся квоту. Запись принимает Idempotency-Key. События уходят вебхуками с HMAC-подписью, повторами, журналом доставки и возможностью переотправки. Sandbox-окружение с тестовыми данными позволяет партнёрам интегрироваться безопасно. Цена — время на проектирование заранее и долгие обязательства по поддержке, ведь когда партнёры зависят от версии, её изменение стоит им реальных денег.

S2Как сохранить согласованность API, если оно охватывает несколько сервисов или очередь?

Внутри одного сервиса я использую транзакции базы. Между сервисами распределённые транзакции редко окупаются, поэтому я проектирую под eventual consistency. Каждый сервис владеет своими данными; изменения публикуются как события через transactional outbox, а потребители обрабатывают их идемпотентно по ID события. Многошаговые бизнес-процессы — заказ, оплата и резервирование товара — становятся сагой: последовательностью локальных шагов с компенсирующими действиями, если более поздний шаг падает, например снять резерв, когда оплата отклонена. API честно показывает это через состояния pending, confirmed и failed плюс эндпоинт статуса или вебхук, а не делает вид, что всё произошло синхронно. Цена — больше состояний для обработки в клиентах и больше мониторинга зависших процессов.

S3Что бы вы мониторили у API в продакшене?

По каждому эндпоинту и клиенту: частоту запросов, долю ошибок с разбивкой на 4xx и 5xx и latency на p50, p95 и p99. Растущая доля 4xx от одного клиента часто означает сломанную интеграцию, а 5xx — проблему на нашей стороне. Я слежу за 429, чтобы понять, хорошо ли настроены лимиты, а также за таймаутами и повторами к зависимостям. Каждый запрос получает request ID, который возвращается в ответе и пишется в структурированные логи, чтобы поддержка могла проследить жалобу клиента до конкретных строк лога, а распределённый трейсинг показывает, куда уходит время между сервисами. Для критичных бизнес-процессов я добавляю синтетические проверки, которые оформляют тестовый заказ. Использование устаревших версий отслеживается, чтобы отключения опирались на данные. Алерты срабатывают на симптомы, которые чувствуют пользователи, а не на каждый всплеск CPU.

Глоссарий

Ресурс
То, что открывает ваш API, например заказ или клиент; идентифицируется URL.
Представление
Формат, в котором передаётся ресурс, обычно JSON, описывающий его текущее состояние.
BOLA / IDOR
Broken object level authorization: пользователи получают доступ к чужим объектам, просто меняя ID.
Mass assignment
Когда API копирует все присланные клиентом поля в модель, позволяя выставлять поля вроде role или price.
Idempotency key
Уникальное значение, сгенерированное клиентом, по которому сервер распознаёт повторный запрос и безопасно его игнорирует.
Курсор
Непрозрачный указатель на позицию в отсортированном списке для надёжной загрузки следующей страницы.
Problem Details
Стандартный JSON-формат ошибок API по RFC 9457, отдаётся как application/problem+json.
Transactional outbox
Сохранение событий в той же транзакции базы, что и данные, с последующей доставкой воркером.
Token bucket
Алгоритм rate limiting, который пополняет лимит с постоянной скоростью и допускает короткие всплески.
OpenAPI
Машиночитаемое описание HTTP API для документации, генерации клиентов и тестирования.

Есть идея продукта?Давайте создадим его.

Расскажите о своей идее, продукте или команде. Обычно отвечаю в течение одного рабочего дня.