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.
| Ключ лимита | Пример политики | От чего защищает |
| по IP | 100 запросов/мин | от парсеров и наивных ботов |
| по пользователю | 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 вычислен на сервере. Клиент не думает — он рисует.
| Критерий | Один общий gateway | BFF на каждый клиент |
| Клиентов | 1–2 похожих | 3+ разных по природе |
| Скорость команд | изменения через общую команду | каждая команда правит свой BFF |
| Дублирование | минимум | есть, и это осознанная цена |
| Риск | «божественный» конфиг | рост числа сервисов |
Как это работает
Путь одного запроса через границу, по шагам:
- Балансировщик провайдера принимает TCP-соединение и отдаёт его одному из подов gateway.
- Gateway терминирует TLS и разбирает HTTP-запрос.
- По пути и методу находит маршрут. Не нашёл —
404, дальше ничего не происходит. - Проверяет токен. Невалиден —
401, внутренние сервисы даже не узнают о запросе. - Считает лимиты (счётчики обычно в Redis, потому что подов gateway несколько и они должны видеть общую картину). Превышен —
429. - Находит живые адреса нужного сервиса через service discovery (об этом — следующий урок) и выбирает инстанс.
- Проксирует запрос с таймаутом, добавляя
trace-idи данные пользователя. - Получает ответ, при необходимости склеивает несколько ответов в один, пишет метрику и отдаёт клиенту.
Технически это 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 трафик. Внутренние вызовы через него гонять не надо.