Backend for Frontend: слой, который знает, кто спрашивает
Паттерн описали в 2015 году ради скорости и автономии команд. Через одиннадцать лет черновик IETF стал настойчиво рекомендовать его ради того, о чём его авторы не писали, — чтобы токен вообще не попадал в браузер.
Полное техническое изложение
TL;DR
- BFF — это маленький сервер, который стоит между вашим экраном и остальными сервисами и работает на этот конкретный экран.
- Первая причина его завести: собрать данные для экрана одним обращением вместо шести. Каждое обращение из браузера стоит времени, и эти времена складываются.
- Вторая причина, появившаяся намного позже: ключ от сессии не попадает туда, откуда его может прочитать JavaScript, — браузер хранит его в куке и прикладывает сам.
- Но от взлома самой страницы BFF не спасает. Он решает, что злоумышленник унесёт с собой, а не сможет ли он навредить прямо сейчас.
Задача, из которой всё выросло
Представьте экран профиля. Чтобы его нарисовать, нужны сами данные пользователя, его закладки, прогресс чтения и ещё пара мелочей. Если браузер спрашивает это по частям, он делает несколько отдельных обращений к серверу — и ждёт ответа на каждое.
Пока вы разрабатываете на своём ноутбуке, ожидание почти нулевое: сервер стоит на той же машине. В реальной сети всё иначе, особенно на мобильном интернете. И вот тут выясняется неприятное: время не «примерно одинаковое», а складывается.
Сколько это стоит на самом деле
Ниже — измерение на настоящем приложении. Задержка в 150 мс на каждый запрос наложена специально: без неё измерение показало бы, что сеть бесплатна, а это и есть та самая иллюзия, которую нужно разрушить.
Один запрос — 165 мс. Шесть запросов — 972 мс. Почти секунда, за которую сервер не сделал ничего сложного: браузер просто шесть раз сходил туда и обратно.
Идея BFF в одном предложении: пусть браузер сделает одно обращение, а все шесть внутренних вызовов сделает сервер, которому до соседних сервисов рукой подать. В том же прогоне обращение к соседнему сервису — уже не из браузера, а с самой машины — заняло меньше миллисекунды.
Важная оговорка: если ваш экран собирается одним запросом, собирать нечего и BFF по этой причине не нужен — вы добавите лишний шаг и ничего не выиграете.
Вторая причина: ключ от двери
Есть второй повод завести такой слой, и он совсем о другом.
Обычное приложение, которое ходит в API само, вынуждено где-то хранить ключ от сессии — токен. Хранить его приходится там, куда дотягивается JavaScript: в localStorage или в обычной куке. А значит, туда же дотянется и чужой скрипт, если его удастся подсунуть на страницу.
BFF позволяет этого не делать. Браузер получает не токен, а куку с пометкой HttpOnly — такую, которую JavaScript прочитать не может в принципе. Настоящий токен остаётся на сервере, и сервер сам подставляет его в запросы дальше.
Вот измерение того, что видит скрипт при трёх способах хранения. Переключайте пробы кнопками.
А теперь неприятная правда
Переключите визуализацию на третью пробу — ту, где скрипт запрашивает закладки.
Он их получает. Токена у него нет, а данные есть.
Причина простая: скрипт выполняется на вашей же странице, а значит, для браузера он — это вы. Браузер послушно прикладывает куку к его запросам, как приложил бы к вашим.
Отсюда правило, которое стоит запомнить именно в такой формулировке:
BFF меняет не вероятность взлома, а его последствия. Украсть ключ и уйти с ним — больше нельзя. Пошалить от вашего имени, пока открыта вкладка, — по-прежнему можно.
Разница здесь не косметическая. Украденный токен работает и после того, как вы закрыли вкладку, из любой точки мира и без вашего участия. Доступ «пока открыта вкладка» заканчивается вместе с вкладкой.
Что ещё обязательно, если сессия живёт в куке
Черновик стандарта IETF, описывающий этот подход, требует трёх вещей, и ни одна из них не опциональна в реальном приложении:
- кука ходит только по защищённому соединению;
- кука недоступна для JavaScript;
- отдельная защита от подделки запросов — само по себе свойство куки от этого не спасает.
Последний пункт часто пропускают. Существует настройка SameSite, которая просит браузер не прикладывать куку к запросам с чужих сайтов, — и её нередко считают достаточной. Спецификация куки говорит прямо противоположное: это полезный дополнительный слой, но не полноценная защита. Поэтому сервер дополнительно выдаёт CSRF-токен, привязанный к сессии: страница получает его отдельным запросом к своему же серверу и прикладывает к каждому изменяющему действию. Форма с чужого сайта ответ вашего сервера прочитать не может — значит, и токена не узнает. От чужого скрипта на вашей же странице это не спасает: он запросит токен так же, как страница.
Проверить это можно прямо в измерении выше: запрос с валидной кукой, но без такого токена получает отказ.
Когда BFF не нужен
Коротко: когда нечего собирать и нечего прятать.
Если у вас один фронтенд, одна команда, один сервис за спиной, экран собирается одним запросом, а токенов в браузере нет по другим причинам — вы добавите лишний сервис, лишний деплой и лишний шаг в цепочке. Sam Newman, автор канонического описания паттерна, оговаривает это отдельно: для приложения с одним только веб-интерфейсом такой слой оправдан, лишь если на сервере действительно есть что агрегировать.
И наоборот: как только у экрана появляется своя команда со своим темпом изменений — или как только в браузере оказывается что-то, чего там быть не должно, — слой начинает окупаться.
TL;DR
- BFF — это не слой между фронтендом и бэкендом «вообще». Это отдельный бэкенд на каждый пользовательский опыт, который принадлежит команде этого опыта.
- Придумали его ради двух вещей: убрать круговые задержки и убрать очередь на изменение общего API. О том, где хранится токен, в исходных текстах нет ни слова.
- Вторая жизнь паттерна началась в стандартах: черновик IETF по OAuth в браузере настойчиво рекомендует BFF, потому что при нём токен физически не попадает в JavaScript.
- Чего BFF не делает: он не спасает от XSS. Скрипт, выполняющийся в вашем origin — в той же тройке «схема + домен + порт», что и страница, — всё так же прочитает данные пользователя — просто чужими руками. Это записано в самом черновике.
Зачем это знать?
Потому что «BFF» стало словом, которым называют что угодно между браузером и сервисами: прокси, шлюз, «наш api-роут в Next.js». Из-за этого разговор об архитектуре превращается в спор о названиях, а в код попадают решения, взятые из статьи про совсем другую задачу.
У паттерна есть авторы, даты публикаций и две разные причины существования, разнесённые на десятилетие. Первая — производительность и автономия команд: 2012 год у Netflix, 2015-й у SoundCloud. Вторая — хранение токенов, и она появилась не у архитекторов, а в тексте IETF. Понимать, какая из двух причин ваша, важнее, чем знать определение: от этого зависит, сколько BFF заводить, кому они принадлежат и что считать провалом.
Модель в голове
Представьте переводчика на переговорах. Универсальный API — это словарь: он честно переводит каждое слово, но чтобы понять фразу, вам придётся заглянуть в него шесть раз, и словарь не знает, что вы вообще-то ведёте переговоры о поставке. Переводчик знает: он слушает всю фразу, сам сверяется с чем нужно и отдаёт вам один готовый ответ — на вашем языке и в вашем контексте.
Ключевое в этой метафоре — чей переводчик. Он нанят вашей стороной и уходит вместе с ней. Это и есть главное свойство BFF: он принадлежит команде фронтенда, а не команде платформы, и меняется вместе с экраном, а не вместе с доменной моделью.
Откуда это на самом деле взялось
История начинается не с BFF, а с Netflix. В июле 2012 года Daniel Jacobson описал, почему их универсальный REST API перестал работать: компания поддерживала, по его словам, более 800 типов устройств, и один и тот же ответ подходил всем одинаково плохо. Вывод в статье сформулирован жёстко — такой API удобен тому, кто его отдаёт, а не тому, кто его потребляет.
Netflix решил это не «ещё одной точкой входа», а сменой владельца кода: UI-команды получили право писать серверные адаптеры под свои экраны. Через полгода Ben Christensen в продолжении описал масштаб — 2+ миллиарда входящих запросов в день — и главный выигрыш: собрать экран одним обращением, чтобы WAN-задержка платилась один раз.
Обратите внимание на то, чего в этих двух текстах нет. В них ни разу не обсуждается, где лежит токен. Это статьи про задержку и про то, кто пишет код.
Само название появилось в сентябре 2015 года: Phil Calçado, инженер SoundCloud, описал их путь — монолит, затем публичный API для всех клиентов сразу, затем упор в три стены. Третья стена самая недооценённая: каждую новую точку входа в API приходилось долго обсуждать, чтобы она не оказалась слишком узкой под конкретный экран. Под новое iOS-приложение команда завела отдельный бэкенд — «разные бэкенды для разных фронтендов».
Через два месяца Sam Newman оформил это в паттерн — и заодно сохранил для нас правило, из которого дальше считают, сколько BFF заводить. Сам он приписывает его Stewart Gleadow, а тот, в свою очередь, Phil Calçado и Mustafa Sezgin:
One experience, one BFF.
Дословно: «один пользовательский опыт — один BFF». Не «один на фронтенд», не «один на команду», не «один на приложение». Единица деления — опыт: если iOS и Android почти одинаковы, им хватит одного BFF; если они разошлись — нужны два. Рядом Newman приводит наблюдение Pete Hodgson: на практике BFF лучше всего ложатся на границы команд, и именно структура команд должна определять их количество, — потому что делить надо то, что кто-то потом сопровождает.
Первая причина: круговые задержки складываются
Аргумент 2012 года выглядит очевидным ровно до тех пор, пока его не измеришь. Сеть в разработке кажется бесплатной, потому что на localhost она почти бесплатная.
Ниже — измерение. Реальный браузер против работающего приложения, задержка 150 мс на запрос, наложенная через Chrome DevTools Protocol; каждое число — медиана трёх прогонов. Пунктир и плитки — арифметика по вашим числам, а не измерение; ползунки нужны, чтобы подставить свои.
Здесь важен не сам факт «шесть запросов дольше одного», а то, во что превращается разница. Шесть последовательных обращений при наложенных 150 мс дали 972 мс по медиане трёх прогонов — больше, чем арифметические 6 × 150 на плитке: к наложенной задержке добавляется работа на обоих концах. Почти секунда, за которую на сервере ничего сложного не произошло. Тот же экран за одним обращением к BFF стоит одну задержку плюс работу на стороне сервера: обращение к соседнему сервису с той же машины обошлось в 0,71 мс по медиане двадцати замеров curl (поле serverLegLoopback).
Отсюда — важная оговорка, о которой в пересказах обычно забывают. Newman высказывает её осторожно — «I suspect», — но по сути прямо: для приложения с одним только веб-интерфейсом BFF оправдан, лишь если на сервере действительно есть что агрегировать. Если ваш экран — это один запрос к одному сервису, вы получите лишний сетевой прыжок и лишний деплой, а взамен ничего.
Вторая причина появилась десятилетием позже
Безопасность в текстах 2015 года упоминается — но совсем другая. У Calçado это первая из трёх проблем универсального API: приходилось всюду проверять OAuth-scope и мешать подделке ключей «официального приложения». Речь о том, кому можно вызывать точку входа, а не о том, где лежит токен. Аргумент про хранение токена пришёл позже и из другого места — из стандартизации.
Черновик IETF «OAuth 2.0 for Browser-Based Applications» (ревизия 27, июль 2026; передан в IESG и ждёт публикации как BCP) разбирает три архитектуры в порядке убывания безопасности, ставит BFF первой и настойчиво рекомендует её для бизнес-приложений, чувствительных приложений и всего, что работает с персональными данными. Её обязанности он определяет тремя пунктами: BFF выступает конфиденциальным OAuth-клиентом, хранит access- и refresh-токены внутри cookie-сессии и сам подставляет нужный токен в запросы к ресурсному серверу.
Черновик формулирует смысл конструкции в разделе 6.1: токены доступны только BFF, и странице извлекать нечего.
Это проверяемое утверждение, а не декларация. Ниже — измерение того, что достаёт скрипт, выполняющийся на странице, при трёх способах хранить сессию. Колонка «за BFF» — дословный вывод прогона против живого стенда (Next.js в продакшен-режиме перед Go-сервисом перед PostgreSQL); две другие сняты тем же способом на отдельной странице, куда синтетический токен положили в localStorage и в куку без HttpOnly.
Первые две пробы подтверждают то, что можно проверить со стороны страницы: ни localStorage, ни document.cookie не отдают скрипту ничего — хотя в банке браузера обе куки лежат. Заодно они объясняют, почему OWASP в Session Management Cheat Sheet прямо запрещает класть токены и идентификаторы сессий в localStorage или sessionStorage: там их видит любой скрипт вашего origin.
А вот третья проба — самое полезное, что есть в этой статье.
Чего BFF не делает
Переключите визуализацию выше на третью пробу. Скрипт, у которого нет токена, спокойно читает закладки пользователя: он просто делает fetch к своему же origin, а куку прикладывает браузер.
Это не дефект реализации. Черновик IETF называет эту атаку прямо — «проксирование запросов через браузер пользователя», раздел 5.1.4: вредоносный код выполняется в том же контексте, что и легитимное приложение, и потому может воспроизводить его поведение. В разборе BFF (раздел 6.1.4.1) этот сценарий остаётся единственным, который паттерн не закрывает, и там же сказано: помешать такому нельзя ничем, кроме недопущения самого выполнения чужого кода.
Практический вывод, который стоит проговорить вслух на любом ревью архитектуры:
BFF меняет последствия XSS, а не вероятность XSS. Украсть долгоживущий токен и уйти с ним в свою инфраструктуру атакующий больше не может. Действовать от имени пользователя, пока открыта вкладка, — по-прежнему может.
Разница реальная и большая: украденный refresh-токен работает после закрытия вкладки, из другой страны и без жертвы. Но продавать BFF как «защиту от XSS» — значит обещать не то, что он делает.
Что стандарт требует от куки
Раз сессия переехала в куку, требования к ней перестают быть делом вкуса. Черновик формулирует их в разделе 6.1.3.2 и 6.1.3.3 нормативной лексикой RFC 2119:
| Требование | Уровень | Что это значит |
|---|---|---|
Secure | MUST | Кука не уходит по открытому HTTP |
HttpOnly | MUST | Куку не видно из document.cookie |
SameSite=Strict | SHOULD | Кука не прикладывается к кросс-сайтовым запросам вообще |
| Защита от CSRF | MUST | Отдельный механизм, не заменяемый предыдущим пунктом |
Последние две строки стоит развести, потому что их постоянно путают. SameSite — это поведение браузера, а не проверка на вашем сервере. rfc6265bis в разделе 5.6.7.1 говорит об этом без обиняков: режим Lax — разумная эшелонированная защита против CSRF, использующих небезопасные методы, но не полноценная защита от CSRF как класса атак. OWASP формулирует то же самое как правило: относиться к SameSite как к слою в глубину и сочетать его с CSRF-токеном, а не полагаться на него одного.
Отсюда — synchronizer token pattern: сервер выдаёт токен, привязанный к сессии, и на каждом изменяющем запросе сверяет пришедшее значение с сохранённым. Кросс-сайтовая форма может заставить браузер приложить куку, но не может прочитать ответ вашего origin — а значит, не узнает токен.
Честное отступление от стандарта: почему здесь Lax, а не Strict
Приложение, на котором сняты измерения выше, ставит SameSite=Lax. Черновик говорит SHOULD ... Strict. Это осознанное отклонение, и вот его причина.
Ссылки на подтверждение почты и сброс пароля приходят письмом. Переход по такой ссылке — кросс-сайтовая навигация верхнего уровня: письмо открыто в почтовом клиенте, а не на вашем сайте. При Strict браузер не приложит куку к этому переходу, и человек, только что подтвердивший адрес, попадёт на страницу, которая считает его гостем. При Lax кука прикладывается к навигациям верхнего уровня безопасными методами — то есть ровно к переходу по ссылке из письма, но не к кросс-сайтовому POST.
SHOULD в RFC 2119 и означает эту ситуацию: требование можно не выполнить, если вы поняли последствия. Последствие здесь — ослабленный первый эшелон защиты от CSRF, и он компенсирован вторым: synchronizer token со сравнением за постоянное время плюс проверка заголовка Origin. Проба «изменяющий запрос без CSRF-заголовка» в измерении выше показывает работу этого второго эшелона: запрос из того же origin, с валидной кукой, но без токена получает 403.
Что здесь важно методологически: отклонение от SHOULD перестаёт быть проблемой ровно тогда, когда оно записано вместе с причиной и с тем, чем его закрыли. Незаписанное отклонение — это просто баг, о котором пока никто не знает.
Сколько заводить BFF и чем за них платят
Правило «один опыт — один BFF» звучит просто и ломается на втором BFF. Newman не прячет проблему: между BFF накапливается дублирование. Чего он делать не советует — сливать их обратно в «общий агрегирующий edge-сервис»: такой сервис, по его наблюдению, раз за разом превращается в раздутый код с перемешанными задачами. То есть это тот самый универсальный API, от которого уходили, только с новым названием.
К самому дублированию он относится спокойно: I am fairly relaxed about duplicated code across services
(я довольно спокойно отношусь к дублированию кода между сервисами). Это не запрет на вынос общего кода — Newman перечисляет и общую библиотеку, и отдельный сервис как варианты, — но предупреждает, что библиотека — главный источник связности, особенно если через неё генерируются клиенты к нижележащим сервисам. Практический смысл: дублирование между BFF — цена, которую платят осознанно, и выносить общее стоит тогда, когда оно уже устоялось, а не при первом совпадении.
Практическая рамка: BFF оправдан, когда у экрана есть свой темп изменений и своя команда. Если у вас один фронтенд, одна команда и один сервис за спиной — вы платите за прыжок и деплой, не получая ни автономии, ни агрегации. В этом случае честнее сказать «у нас серверный слой для сессий», чем называть его BFF: слово тянет за собой ожидания, которых конструкция не оправдывает.
Чем BFF не является
Это не API Gateway. Шлюз — инфраструктурный компонент, общий для всех потребителей: маршрутизация, лимиты, терминация TLS, аутентификация на входе. BFF — прикладной код, принадлежащий одной команде и знающий про её экраны. Они не конкурируют и часто стоят вместе: шлюз на входе, BFF за ним.
Это не «просто прокси». Прокси, который один в один пересылает запросы, не даёт ни агрегации, ни автономии — двух вещей, ради которых паттерн придумали. Он может быть полезен ради токенов, но тогда это следует называть своим именем, а не паттерном 2015 года.
Это не GraphQL. GraphQL решает соседнюю задачу — «клиент сам описывает, что ему нужно», — и решает её схемой, а не выделенным бэкендом на каждый опыт. Заметьте, что Netflix в 2012 году выбрал третий вариант: не универсальный API и не язык запросов, а серверные адаптеры, которые пишут UI-команды. Все три подхода отвечают на один вопрос, но по-разному распределяют, кто пишет код и кто за него отвечает.
Своя версия и общая трасса
У BFF, привязанного к одному клиенту, две эксплуатационные особенности, которые обычно замечают поздно.
Первая — версионирование. Microsoft формулирует её как следствие самого паттерна:
Evaluate your optimal number of services depending on the associated costs.
Maintaining and deploying more services means increased operational overhead.
Each individual service has its own life cycle, deployment and maintenance
requirements, and security needs.
Оценивайте оптимальное число сервисов исходя из связанных с ними затрат. Сопровождение и развёртывание большего числа сервисов означает возросшие эксплуатационные накладные расходы. У каждого отдельного сервиса свой жизненный цикл, свои требования к развёртыванию и сопровождению и свои потребности в безопасности.
«Свой жизненный цикл» у BFF означает цикл его клиента. Веб-фронтенд обновляется одновременно с BFF — старую версию можно снять в тот же день. Мобильное приложение обновляется у пользователя, и не у всех: старая версия ходит в BFF месяцами. Это разные режимы совместимости у сервисов, которые выглядят одинаково.
Придумывать формат отказа от старого не нужно, для него есть нормативный документ:
The Deprecation HTTP response header field is used to signal to consumers of a
resource (identified by a URI) that the resource will be or has been deprecated.
Заголовок ответа Deprecation используется, чтобы сообщить потребителям ресурса (заданного через URI), что ресурс будет объявлен устаревшим или уже объявлен.
Заголовок несёт дату, а не флаг: Deprecation: @1688169599. Рядом с ним стоит
Sunset — дата отключения, — и между ними RFC устанавливает порядок:
The timestamp given in the Sunset HTTP header field MUST NOT be earlier than the one given in the Deprecation header field
(Отметка времени в заголовке Sunset НЕ ДОЛЖНА быть раньше, чем отметка в заголовке Deprecation).
В примере самого документа между объявлением и отключением проходит год — и это
хороший ориентир для мобильного клиента, для которого «мы выкатили» не значит «у
всех обновилось».
Вторая — наблюдаемость, и тут у BFF особая роль. Один клиентский запрос разветвляется в несколько внутренних, то есть BFF — то самое место, где трасса может порваться.
Distributed tracing is a methodology implemented by tracing tools to follow,
analyze and debug a transaction across multiple software components. Typically, a
distributed trace traverses more than one component which requires it to be
uniquely identifiable across all participating systems.
Распределённая трассировка — методика, реализуемая инструментами трассировки, чтобы прослеживать, анализировать и отлаживать транзакцию через несколько программных компонентов. Обычно распределённая трасса проходит более чем через один компонент, что требует её однозначной идентифицируемости во всех участвующих системах.
Требование к посреднику стандарт разделяет на два уровня, и минимальный — обязателен:
At a minimum they MUST propagate the traceparent and tracestate headers and
guarantee traces are not broken. This behavior is also referred to as forwarding
a trace.
Как минимум они ОБЯЗАНЫ распространять заголовки traceparent и tracestate и гарантировать, что трассы не рвутся. Это поведение также называют пересылкой трассы.
То есть BFF, не пересылающий traceparent дальше, рвёт трассу ровно там, где она
нужнее всего: на разветвлении. Формат заголовка фиксирован и разбирается
тривиально — traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01,
четыре поля: версия, идентификатор трассы, идентификатор родителя, флаги.
Отдельная оговорка, которая противоречит соблазну «сложим всю наблюдаемость в BFF»:
The BFF service should only handle client-specific logic related to a specific
user experience. Cross-cutting features, such as monitoring and authorization,
should be abstracted to maintain efficiency.
Сервис BFF должен обрабатывать только специфичную для клиента логику, относящуюся к конкретному пользовательскому опыту. Сквозные возможности, такие как мониторинг и авторизация, следует выносить наружу, чтобы сохранять эффективность.
Пересылать контекст — обязанность BFF. Быть местом, где живёт мониторинг всей системы, — нет: тогда каждый новый клиентский опыт потребует копии этой машинерии.
Federation: то же лекарство, разрез по другой оси
Выше сказано, что GraphQL решает соседнюю задачу схемой, а не выделенным бэкендом. У этого утверждения есть продолжение, о котором стоит знать при выборе: у GraphQL есть свой ответ на ту же боль, что породила BFF, — очередь команд на изменение общего API.
Начинается он с прямо противоположной посылки:
To get the most out of GraphQL, your organization should expose a single graph
that provides a unified interface for querying any combination of your backing
data sources.
Чтобы получить максимум от GraphQL, ваша организация должна выставлять единый граф, дающий унифицированный интерфейс для запросов к любой комбинации ваших источников данных.
Единый граф — это ровно тот универсальный API на всех потребителей, от которого уходили Netflix и SoundCloud. Federation не отменяет его, а решает вторую половину задачи — автономию команд — внутри него:
Apollo Federation encourages a design principle called separation of concerns.
This enables different teams to work on different products and features within a
single graph, without interfering with each other.
Apollo Federation поощряет принцип проектирования, называемый разделением ответственности. Он позволяет разным командам работать над разными продуктами и возможностями внутри единого графа, не мешая друг другу.
Вот и весь критерий выбора, и он в одну строку: federation делит по владельцу домена, BFF — по потребителю. Обе конструкции убирают очередь на изменение общего API. Federation оставляет один интерфейс на всех клиентов, а автономию даёт командам, владеющим данными. BFF заводит интерфейс на каждого клиента, а автономию даёт командам, владеющим опытом.
Отсюда и практическое правило. Если больно оттого, что у клиентов разные потребности к форме ответа — это BFF. Если больно оттого, что несколько команд пишут в одну схему — это federation. Если больно и от того, и от другого, они совмещаются: BFF поверх суперграфа.
Цену federation надо включать в сравнение честно, потому что она не в шлюзе, а в каждом нижележащем сервисе:
For a GraphQL service to operate as an Apollo Federation 2 subgraph, it must do
all of the following: Automatically extend its schema with all definitions listed
in Subgraph schema additions; Correctly resolve the Query._service enhanced
introspection field; Provide a mechanism for subgraph developers to resolve
entity fields via the Query._entities field; Apply the @link directive to the
schema type.
Чтобы GraphQL-сервис работал как подграф Apollo Federation 2, он обязан сделать всё следующее: автоматически расширить свою схему всеми определениями, перечисленными в „Дополнениях к схеме подграфа“; корректно разрешать расширенное интроспекционное поле Query._service; предоставить разработчикам подграфа механизм разрешения полей сущностей через поле Query._entities; применить директиву @link к типу schema.
Каждый сервис перестаёт быть просто HTTP-сервисом и обязан реализовать спецификацию. BFF такого не требует ни от кого: нижележащие сервисы о нём не знают.
И симметричная честность в другую сторону — Microsoft прямо допускает, что при GraphQL отдельный слой может быть не нужен:
With GraphQL, the querying mechanism eliminates the need for a separate BFF layer
because it allows clients to request the data that they need without relying on
predefined endpoints. […] Evaluate whether you need this pattern. For example, if
your organization uses GraphQL with frontend specific resolvers, BFF services
might not add value to your applications.
При GraphQL механизм запросов устраняет необходимость в отдельном слое BFF, поскольку позволяет клиентам запрашивать нужные им данные, не опираясь на заранее заданные точки входа. […] Оцените, нужен ли вам этот паттерн вообще. Например, если ваша организация использует GraphQL с резолверами под конкретный фронтенд, BFF-сервисы могут не давать вашим приложениям никакой добавленной ценности.
Оговорка «ради сессии» из следующего раздела к этому не относится: токен в
HttpOnly-куке нужен независимо от того, REST у вас или GraphQL.
Как это выглядит в этом приложении
Читатель этого сайта не ходит в Go-сервис напрямую: браузер знает только маршруты Next.js под /api, а токен сессии живёт в HttpOnly-куке, к которой у скриптов нет доступа. Модуль, из которого делаются исходящие вызовы, помечен server-only — импорт его из клиентского компонента ломает сборку, а не проходит ревью.
Это BFF во втором смысле — ради сессии, а не ради агрегации. Но не буквально по черновику: OAuth здесь нет, а вместо access- и refresh-токенов — непрозрачный идентификатор сессии, который хранится на сервере и проверяется по каждому запросу (ADR-005). По собственному критерию выше этот слой ближе к «серверному слою для сессий», чем к BFF образца 2015 года; слово оставлено потому, что его так называет черновик IETF, а не потому, что здесь есть агрегация. Агрегации здесь почти нет: маршруты в основном один к одному соответствуют вызовам Go-сервиса. По критерию Newman это как раз тот случай, когда BFF ради агрегации был бы не нужен, — и он здесь стоит не ради неё.
Что здесь стандарт, а что — решение команды
| Утверждение | Статус |
|---|---|
Токен не должен лежать в localStorage | Требование OWASP |
Кука сессии — Secure и HttpOnly | MUST черновика IETF |
SameSite=Strict | SHOULD черновика IETF |
| Отдельная защита от CSRF обязательна | MUST черновика IETF |
SameSite в одиночку от CSRF не защищает | Прямо сказано в rfc6265bis |
| «Один опыт — один BFF» | Рекомендация Newman, не стандарт |
| BFF нужен веб-приложению только при заметной агрегации | Оговорка Newman, не стандарт |
| Дублирование между BFF терпимо | Позиция Newman, спорная |
Lax вместо Strict в этом приложении | Решение команды, обосновано выше |
Разделение здесь не формальность. Первые пять строк — то, за нарушение чего вам справедливо укажут на спецификацию. Следующие три — опыт конкретных людей, полученный на конкретных системах, и их можно оспаривать. Последняя — ваша собственная ответственность, и единственное, что делает её приемлемой, — записанная причина.
Что стоит помнить
Паттерн, у которого две несвязанные причины существования, опаснее обычного: легко внедрить его ради одной и решить, что получил обе. BFF, поставленный ради токенов, не ускорит экран сам по себе. BFF, поставленный ради агрегации, не защитит токен, если вы всё равно отдаёте его в JavaScript.
И ни один из них не остановит XSS — он лишь решает, что именно атакующий унесёт с собой, когда вкладка закроется.
Чем измерено
Числа этой статьи получены этими скриптами. Каждый открывается прямо отсюда — вместе с записью прогона: на чём считали, что получилось и с каким разбросом.
Это не пересказ и не отдельный текст: всё ниже взято из самой статьи — её выжимка, заголовки разборов, колонка «на самом деле» и таблица версий. Поэтому разойтись со статьёй эти тезисы не могут.
Суть
- BFF — это не слой между фронтендом и бэкендом «вообще». Это отдельный бэкенд на каждый пользовательский опыт, который принадлежит команде этого опыта.
- Придумали его ради двух вещей: убрать круговые задержки и убрать очередь на изменение общего API. О том, где хранится токен, в исходных текстах нет ни слова.
- Вторая жизнь паттерна началась в стандартах: черновик IETF по OAuth в браузере настойчиво рекомендует BFF, потому что при нём токен физически не попадает в JavaScript.
- Чего BFF не делает: он не спасает от XSS. Скрипт, выполняющийся в вашем origin — в той же тройке «схема + домен + порт», что и страница, — всё так же прочитает данные пользователя — просто чужими руками. Это записано в самом черновике.
На самом деле
- Он меняет последствия XSS, а не вероятность. Токен действительно не достать из браузера, но скрипт выполняется внутри вашего origin и может слать запросы к BFF от имени пользователя — куку приложит браузер. Черновик IETF называет это проксированием запросов через браузер пользователя (раздел 5.1.4), а в разборе BFF (6.1.4.1) оставляет её единственным работающим сценарием и говорит, что помешать этому нельзя ничем, кроме недопущения выполнения чужого кода.
- О хранении токенов в первоисточниках 2012–2015 годов (Netflix, Calçado, Newman) нет ни слова: там речь о круговых задержках и о том, кто имеет право менять код точки входа. Безопасность у Calçado упоминается, но другая — проверки OAuth-scope и подделка ключей «официального приложения», то есть кому можно вызывать точку входа. Аргумент про то, где лежит токен, появился десятилетием позже — в черновике IETF по OAuth в браузере.
- rfc6265bis в разделе 5.6.7.1 говорит, что
Lax— разумная эшелонированная защита против CSRF на небезопасных методах, но не полноценная защита от CSRF как класса. OWASP формулирует то же самое как правило: сочетатьSameSiteс CSRF-токеном, а не полагаться на него одного. Черновик IETF выносит это в отдельноеMUST. - Шлюз общий для всех потребителей и живёт в инфраструктуре: маршрутизация, лимиты, TLS. BFF принадлежит одной продуктовой команде и знает про её экраны. Они не взаимозаменяемы и часто стоят вместе — шлюз на входе, BFF за ним.
- Newman оговаривает это отдельно (осторожным «I suspect»): для приложения с одним только веб-интерфейсом BFF оправдан, лишь если на сервере действительно есть заметная агрегация. Без неё вы получаете лишний сетевой прыжок и лишний деплой. Отдельная история — если слой нужен ради токенов: тогда это верное решение, но по другой причине, и называть его стоит честно.
Что разобрано
- Зачем это знать?
- Модель в голове
- Откуда это на самом деле взялось
- Первая причина: круговые задержки складываются
- Вторая причина появилась десятилетием позже
- Чего BFF не делает
- Что стандарт требует от куки
- Честное отступление от стандарта: почему здесь `Lax`, а не `Strict`
- Сколько заводить BFF и чем за них платят
- Чем BFF не является
- Своя версия и общая трасса
- Federation: то же лекарство, разрез по другой оси
- Как это выглядит в этом приложении
- Что здесь стандарт, а что — решение команды
- Что стоит помнить
- Чем измерено
Унести в работу
- Не продавать BFF как защиту от XSS: он решает, что злоумышленник унесёт с собой, а не сможет ли он навредить прямо сейчас.
- Не заводить BFF «потому что паттерн хороший»: первая его причина — собрать экран одним обращением вместо шести, и если этой задачи нет, нет и повода.
- Заводить по BFF на экран, а не один на всех: тем он и отличается от API Gateway.
- Не считать
SameSiteзакрытым CSRF: куку браузер прикладывает сам, и защиту от подделки запроса надо ставить отдельно.
Частые заблуждения
«BFF защищает от XSS».
Он меняет последствия XSS, а не вероятность. Токен действительно не достать из браузера, но скрипт выполняется внутри вашего origin и может слать запросы к BFF от имени пользователя — куку приложит браузер. Черновик IETF называет это проксированием запросов через браузер пользователя (раздел 5.1.4), а в разборе BFF (6.1.4.1) оставляет её единственным работающим сценарием и говорит, что помешать этому нельзя ничем, кроме недопущения выполнения чужого кода.
«BFF придумали ради безопасности».
О хранении токенов в первоисточниках 2012–2015 годов (Netflix, Calçado, Newman) нет ни слова: там речь о круговых задержках и о том, кто имеет право менять код точки входа. Безопасность у Calçado упоминается, но другая — проверки OAuth-scope и подделка ключей «официального приложения», то есть кому можно вызывать точку входа. Аргумент про то, где лежит токен, появился десятилетием позже — в черновике IETF по OAuth в браузере.
«Поставили SameSite — CSRF закрыт».
rfc6265bis в разделе 5.6.7.1 говорит, что Lax — разумная эшелонированная защита против CSRF на небезопасных методах, но не полноценная защита от CSRF как класса. OWASP формулирует то же самое как правило: сочетать SameSite с CSRF-токеном, а не полагаться на него одного. Черновик IETF выносит это в отдельное MUST.
«BFF — это то же самое, что API Gateway, только модное».
Шлюз общий для всех потребителей и живёт в инфраструктуре: маршрутизация, лимиты, TLS. BFF принадлежит одной продуктовой команде и знает про её экраны. Они не взаимозаменяемы и часто стоят вместе — шлюз на входе, BFF за ним.
«Раз паттерн хороший, BFF нужен любому веб-приложению».
Newman оговаривает это отдельно (осторожным «I suspect»): для приложения с одним только веб-интерфейсом BFF оправдан, лишь если на сервере действительно есть заметная агрегация. Без неё вы получаете лишний сетевой прыжок и лишний деплой. Отдельная история — если слой нужен ради токенов: тогда это верное решение, но по другой причине, и называть его стоит честно.
Проверка знаний
В приложении сессия лежит в HttpOnly-куке за BFF. На странице сработала XSS. Что атакующий может сделать?
Источники и что читать дальше
12 ИСТОЧНИКОВ
- Phil Calçado — The Back-end for Front-end Pattern (BFF)Источник. 18 сентября 2015. Инженер SoundCloud описывает, как их монолит превратился в one-size-fits-all API и почему под новое iOS-приложение завели отдельный бэкенд. Текст, из которого пошло само название.https://philcalcado.com/2015/09/18/the_back_end_for_front_end_pattern_bff.html
- Sam Newman — Backends For FrontendsИсточник. Каноническое описание паттерна (первая публикация — 18 ноября 2015). Здесь собраны правило «one experience, one BFF» (один интерфейс — один BFF) (Newman приводит его от Stewart Gleadow), наблюдение Pete Hodgson про границы команд и предупреждение о том, чем кончаются попытки слить BFF обратно в общий edge-сервис.https://samnewman.io/patterns/architectural/bff/
- Daniel Jacobson — Embracing the Differences: Inside the Netflix API RedesignИсточник. Netflix TechBlog, 9 июля 2012. Отказ от универсального REST API в пользу адаптеров, которые пишут сами UI-команды. Здесь же цифра «более 800 типов устройств».https://netflixtechblog.com/embracing-the-differences-inside-the-netflix-api-redesign-15fd8b3dc49d
- Ben Christensen — Optimizing the Netflix APIИсточник. Netflix TechBlog, 15 января 2013. Продолжение той же истории: «2+ миллиарда входящих запросов в день» и объяснение, почему WAN-задержку выгодно платить один раз.https://netflixtechblog.com/optimizing-the-netflix-api-5c9ac715cf19
- draft-ietf-oauth-browser-based-apps — OAuth 2.0 for Browser-Based ApplicationsОфициальная документация. Черновик IETF, ревизия 27 (6 июля 2026), передан в IESG и ждёт публикации как BCP. Раздел 6.1 описывает BFF как архитектуру и формулирует требования к кукам и CSRF; 5.1.4 разбирает атаку, которую паттерн не закрывает, а 6.1.4.1 честно оставляет её единственным работающим сценарием.https://datatracker.ietf.org/doc/html/draft-ietf-oauth-browser-based-apps-27
- draft-ietf-httpbis-rfc6265bis — Cookies: HTTP State Management MechanismОфициальная документация. Рабочая версия замены RFC 6265. Раздел 5.6.7.1 определяет режимы SameSite и прямо говорит, что Lax — не полноценная защита от CSRF.https://httpwg.org/http-extensions/draft-ietf-httpbis-rfc6265bis.html
- OWASP — Cross-Site Request Forgery Prevention Cheat SheetОфициальная документация. Synchronizer Token Pattern, проверка Origin/Referer и раздел о том, когда SameSite достаточно самой по себе (спойлер: почти никогда).https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html
- OWASP — Session Management Cheat SheetОфициальная документация. Требования к идентификатору сессии: минимум 64 бита энтропии, флаги Secure и HttpOnly, прямой запрет хранить токены в localStorage и sessionStorage.https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html
- RFC 9745 — The Deprecation HTTP Response Header FieldОфициальная документация. Нормативный способ объявить версию устаревшей: «The Deprecation HTTP response header field is used to signal to consumers of a resource (identified by a URI) that the resource will be or has been deprecated» (Заголовок ответа Deprecation используется, чтобы сообщить потребителям ресурса, заданного через URI, что ресурс будет объявлен устаревшим или уже объявлен). Значение — дата, а не флаг. Порядок относительно
Sunsetзадан нормативно: «The timestamp given in the Sunset HTTP header field MUST NOT be earlier than the one given in the Deprecation header field» (Отметка времени в заголовке Sunset НЕ ДОЛЖНА быть раньше, чем отметка в заголовке Deprecation). В примере самого документа между этими датами проходит год.https://www.rfc-editor.org/rfc/rfc9745.html - W3C Trace Context (Recommendation)Официальная документация. Минимальное требование к посреднику, из-за которого BFF обязан пересылать заголовки: «At a minimum they MUST propagate the traceparent and tracestate headers and guarantee traces are not broken» (Как минимум они ОБЯЗАНЫ распространять заголовки traceparent и tracestate и гарантировать, что трассы не рвутся). Формат
traceparentфиксирован: версия, идентификатор трассы, идентификатор родителя, флаги.https://www.w3.org/TR/trace-context/ - Microsoft Azure Architecture Center — Backends for FrontendsОфициальная документация. Откуда берётся требование к версионированию: «Each individual service has its own life cycle, deployment and maintenance requirements, and security needs» (У каждого отдельного сервиса свой жизненный цикл, свои требования к развёртыванию и сопровождению и свои потребности в безопасности). И граница ответственности BFF: «Cross-cutting features, such as monitoring and authorization, should be abstracted to maintain efficiency» (Сквозные возможности, такие как мониторинг и авторизация, следует выносить наружу, чтобы сохранять эффективность). Оттуда же признание, что при GraphQL слой BFF может быть не нужен вовсе.https://learn.microsoft.com/en-us/azure/architecture/patterns/backends-for-frontends
- Apollo — Introduction to Federation и Subgraph SpecificationОфициальная документация. Посылка, противоположная BFF: «your organization should expose a single graph that provides a unified interface» (ваша организация должна выставлять единый граф, дающий унифицированный интерфейс), и то, ради чего federation придумана: «This enables different teams to work on different products and features within a single graph, without interfering with each other» (Он позволяет разным командам работать над разными продуктами и возможностями внутри единого графа, не мешая друг другу). Цена — в спецификации подграфа: каждый нижележащий сервис обязан расширить схему, разрешать
Query._serviceиQuery._entitiesи применить директиву@link.https://www.apollographql.com/docs/federation/