API Gateway и BFF

Почему мобильное приложение не должно знать про двенадцать ваших сервисов — и что стоит между ними.

API Gateway — сервис на границе системы, который принимает все внешние запросы, решает, в какой внутренний сервис их отправить, и берёт на себя сквозные задачи: TLS, аутентификацию, лимиты, логирование.

Зачем это нужно на практике

Представьте интернет-магазин, распиленный на сервисы: каталог, корзина, заказы, платежи, отзывы, профиль, склад, уведомления. Экран «Мой заказ» в мобильном приложении показывает статус доставки, состав заказа с картинками товаров, сумму платежа и кнопку «оставить отзыв». Данные для одного экрана лежат в четырёх разных сервисах.

Что произойдёт, если приложение пойдёт к ним напрямую?

  • Медленно. Четыре запроса по мобильной сети, где один round-trip легко стоит 200–300 мс. Экран собирается полторы секунды вместо трёхсот миллисекунд.
  • Хрупко. Приложение знает адреса всех сервисов. Разделили сервис заказов на два — выкатывайте новую версию в App Store и ждите, пока обновятся пользователи. А те, кто не обновится, будут ходить на старые адреса ещё год.
  • Дорого. Каждый сервис торчит в интернет: каждому нужен TLS-сертификат, защита от ботов, проверка токена, настройка CORS. Восемь команд восемь раз пишут одно и то же — и семь раз ошибаются.
  • Небезопасно. Внутренние сервисы обычно проектируют «для своих»: без жёсткой валидации, с отладочными ручками. Выставлять их наружу — приглашение.

Gateway решает ровно эту боль: клиент знает один адрес, а внутренняя топология остаётся вашим личным делом и может меняться хоть каждую неделю.

Что обычно делает gateway

ОбязанностьЧто это значит
Маршрутизация/api/orders/* идёт в сервис заказов, /api/catalog/* — в каталог
TLS-терминациясертификат живёт в одном месте, внутри кластера — свой транспорт
Аутентификацияпроверка JWT или сессии до того, как запрос коснулся бизнес-логики
Rate limitingлимиты по пользователю, IP, API-ключу
Агрегацияодин внешний запрос — несколько внутренних, ответ склеивается
Трансформацияснаружи REST/JSON, внутри gRPC или очередь
Наблюдаемостьединая точка, где рождается trace-id и считается трафик

Типичная конфигурация маршрутов выглядит примерно так (синтаксис у Kong, Traefik и Spring Cloud Gateway разный, идея одна):

routes:
  - id: orders
    path: /api/v1/orders/**
    upstream: http://orders.prod.svc.cluster.local:8080
    auth: jwt          # без валидного токена дальше не пройдёт
    rate_limit: 60/min # на пользователя
    timeout: 2s

  - id: catalog
    path: /api/v1/catalog/**
    upstream: http://catalog.prod.svc.cluster.local:8080
    auth: none         # публичный каталог, токен не нужен
    rate_limit: 600/min
    timeout: 1s
    cache: 30s

Аутентификация на границе

Смысл в том, чтобы дорогую и однообразную работу сделать один раз. Gateway проверяет подпись JWT, срок жизни, отзыв — и передаёт дальше уже разобранную личность: заголовки вроде X-User-Id и X-Scopes или внутренний токен покороче.

Здесь новичков подстерегает ловушка. Раз gateway проверил токен — значит, внутренние сервисы могут слепо доверять заголовку X-User-Id? Только если физически невозможно обратиться к сервису в обход gateway. А в реальном кластере это редко так: соседний под, скомпрометированная библиотека, забытый отладочный порт — и злоумышленник шлёт свой X-User-Id: 1 прямо в сервис платежей. Поэтому взрослые системы делают одно из двух: либо прокидывают исходный токен и сервис перепроверяет подпись (это дёшево — просто криптография, без похода в базу), либо закрывают внутренний трафик mTLS и авторизацией на уровне service mesh. Аутентификация на границе — это оптимизация, а не единственный рубеж обороны.

Лимиты: rate limiting

Лимит на границе — самая дешёвая защита от того, чтобы один клиент не утопил всю систему. Обычно это алгоритм «дырявого ведра» (token bucket): у ключа есть запас токенов, они восполняются с фиксированной скоростью, запрос без токена получает 429 Too Many Requests.

Ключ лимитаПример политикиОт чего защищает
по IP100 запросов/минот парсеров и наивных ботов
по пользователю60 запросов/минот взбесившегося клиента и от абьюза
по API-ключу партнёрапо тарифу: 10 000/суткиот того, что партнёр съест всю ёмкость
по эндпоинтуPOST /login — 5/мин на IPот перебора паролей

Обязательно отдавайте вместе с 429 заголовок Retry-After: без него клиент будет долбиться в стену и сделает вам DDoS собственными руками.

BFF: Backend for Frontend

Один gateway на всех хорош, пока клиенты похожи. Но веб-версия хочет много данных и подробностей (широкий экран, быстрая сеть), мобильное приложение — минимум байт и минимум запросов (батарея, 3G в метро), а партнёрское API — стабильный, годами не меняющийся контракт. Пытаться удовлетворить всех троих одним набором ручек — значит получить «универсальный» API, неудобный никому.

BFF — отдельный тонкий backend для каждого типа клиента, который собирает и подрезает данные именно под его экраны. У мобильного приложения свой BFF, у веба — свой, у партнёров — свой.

Ключевая деталь, которую часто упускают: BFF принадлежит команде клиента. Фронтендеры сами меняют его вместе с экраном — не заводя тикет в чужую команду и не дожидаясь их спринта. Это не «ещё один слой», это способ убрать межкомандную блокировку.

Мобильный BFF на запрос GET /mobile/orders/42 сходит в четыре сервиса и вернёт ровно то, что нужно экрану:

{
  "id": 42,
  "status": "in_delivery",
  "eta": "2026-07-16T14:00:00Z",
  "total": "4 980 ₽",
  "items": [
    { "title": "Кофеварка Bosch", "thumb": "https://cdn/…/s.webp", "qty": 1 }
  ],
  "can_review": false
}

Заметьте: сумма уже отформатирована, картинка — маленькая превьюшка, а флаг can_review вычислен на сервере. Клиент не думает — он рисует.

КритерийОдин общий gatewayBFF на каждый клиент
Клиентов1–2 похожих3+ разных по природе
Скорость командизменения через общую командукаждая команда правит свой BFF
Дублированиеминимуместь, и это осознанная цена
Риск«божественный» конфигрост числа сервисов

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

Путь одного запроса через границу, по шагам:

  1. Балансировщик провайдера принимает TCP-соединение и отдаёт его одному из подов gateway.
  2. Gateway терминирует TLS и разбирает HTTP-запрос.
  3. По пути и методу находит маршрут. Не нашёл — 404, дальше ничего не происходит.
  4. Проверяет токен. Невалиден — 401, внутренние сервисы даже не узнают о запросе.
  5. Считает лимиты (счётчики обычно в Redis, потому что подов gateway несколько и они должны видеть общую картину). Превышен — 429.
  6. Находит живые адреса нужного сервиса через service discovery (об этом — следующий урок) и выбирает инстанс.
  7. Проксирует запрос с таймаутом, добавляя trace-id и данные пользователя.
  8. Получает ответ, при необходимости склеивает несколько ответов в один, пишет метрику и отдаёт клиенту.

Технически это Nginx, Envoy, Kong, Traefik, Spring Cloud Gateway, YARP или ingress-контроллер в Kubernetes. Важно разделять: gateway работает с north-south трафиком (снаружи внутрь), а вызовы сервисов между собой — это east-west трафик, и им обычно занимается service mesh или клиентские библиотеки. Гонять внутренние вызовы через внешний gateway — распространённая и дорогая ошибка: вы добавляете лишний сетевой хоп и превращаете границу в узкое место.

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

  • Gateway превращается в монолит. Самая опасная. Сначала в него добавили «маленькую проверку скидки», потом склейку заказа, потом расчёт корзины — и вот бизнес-логика живёт на границе. Признаки: любое изменение фичи требует правки gateway; его релиз ждут пять команд; в конфиге тысячи строк, и никто не знает, что сломается. Лечение — жёсткое правило: на границе только сквозные, доменно-независимые вещи. Нужна доменная сборка данных — это BFF или сервис-агрегатор, у которого есть владелец и свой релизный цикл.
  • Единая точка отказа. Упал gateway — упало всё. Минимум два-три инстанса, без sticky-состояния в памяти, health-check и автоперезапуск.
  • BFF на каждый чих. Три клиента — три BFF, это нормально. Пятнадцать BFF на пятнадцать экранов — это уже наносервисы и операционный ад.
  • Слепое доверие заголовкам. Разобрали токен на границе и решили, что внутри проверять нечего. См. выше — так теряют деньги.
  • Каскад таймаутов. У gateway таймаут 30 секунд, у сервиса — 60. Клиент давно отвалился, а система продолжает молотить и держать соединения. Правило: таймаут снаружи всегда меньше, чем сумма внутренних, и никогда не «бесконечность».
  • Логика ретраев без идемпотентности. Gateway «на всякий случай» повторяет неудавшийся POST /payments — и клиент платит дважды. Ретраи на границе допустимы только для безопасных методов; всё остальное — тема урока про идемпотентность.

Итоги

  • API Gateway — единая точка входа: маршрутизация, TLS, аутентификация, лимиты, наблюдаемость. Клиент знает один адрес, а внутренняя структура остаётся вашей.
  • Аутентификация и rate limiting на границе экономят силы, но не отменяют защиту внутри: сервис не должен слепо верить заголовкам.
  • BFF — отдельный backend под каждый тип клиента, которым владеет команда клиента. Он убирает лишние round-trip и межкомандные блокировки.
  • Главный риск — превратить gateway в новый монолит. На границе живёт только сквозная логика; доменная — в сервисах и BFF.
  • Gateway — про north-south трафик. Внутренние вызовы через него гонять не надо.
Проверьте себя
1. Что из перечисленного НЕ должно жить в API Gateway?
AТерминация TLS и проверка JWT
BОграничение частоты запросов (rate limiting)
CРасчёт скидки на заказ по правилам маркетинга
DМаршрутизация запросов по пути в нужный сервис
2. Зачем нужен паттерн BFF (Backend for Frontend)?
AЧтобы каждый микросервис имел собственную базу данных
BЧтобы дать каждому типу клиента (мобильному, веб, партнёрам) свой backend, собирающий данные именно под его экраны
CЧтобы заменить собой service discovery
DЧтобы хранить секреты приложения отдельно от кода