Deep Engineering
Поиск
Продвинутый·Опубликовано·20 МИН

Backend for Frontend: слой, который знает, кто спрашивает

Паттерн описали в 2015 году ради скорости и автономии команд. Через одиннадцать лет черновик IETF стал настойчиво рекомендовать его ради того, о чём его авторы не писали, — чтобы токен вообще не попадал в браузер.

Полное техническое изложение

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 для всех клиентов сразу, затем упор в три стены. Третья стена самая недооценённая: каждый новый эндпоинт приходилось долго обсуждать, чтобы он не оказался слишком узким под конкретный экран. Под новое iOS-приложение команда завела отдельный бэкенд — «разные бэкенды для разных фронтендов».

Через два месяца Sam Newman оформил это в паттерн — и заодно сохранил для нас правило, которое цитируют чаще всего. Сам он приписывает его Stewart Gleadow, а тот, в свою очередь, Phil Calçado и Mustafa Sezgin:

One experience, one BFF.

Stewart Gleadow, цит. по Sam Newman, Backends For Frontends

Дословно: «один пользовательский опыт — один BFF». Не «один на фронтенд», не «один на команду», не «один на приложение». Единица деления — опыт: если iOS и Android почти одинаковы, им хватит одного BFF; если они разошлись — нужны два. Рядом Newman приводит наблюдение Pete Hodgson: на практике BFF лучше всего ложатся на границы команд, и именно структура команд должна определять их количество, — потому что делить надо то, что кто-то потом сопровождает.

Первая причина: круговые задержки складываются

Аргумент 2012 года выглядит очевидным ровно до тех пор, пока его не измеришь. Сеть в разработке кажется бесплатной, потому что на localhost она почти бесплатная.

Ниже — измерение. Реальный браузер против работающего приложения, задержка 150 мс на запрос, наложенная через Chrome DevTools Protocol; каждое число — медиана трёх прогонов. Пунктир и плитки — арифметика по вашим числам, а не измерение; ползунки нужны, чтобы подставить свои.

16531747797201800 мс12 запросов

6 запросов из браузера, последовательно

900 мс

6 × 150 мс. Задержка платится столько раз, сколько сделано обращений.

один запрос к BFF, 6 — внутри

154.3 мс

150 мс + 6 × 0.71 мс. Второе слагаемое — измеренная медиана одного обращения к Go-сервису с той же машины.

Рис. 2. Точки — измерения: реальный браузер против работающего приложения, задержка 150 мс на запрос, наложенная через Chrome DevTools Protocol, медиана трёх прогонов. Они видны, только когда ползунок стоит на 150 мс, — при других значениях показывать их было бы враньём. Пунктир и обе плитки — арифметика, которую можно проверить в уме: она считается по вашим числам и измерением не является. Точки лежат чуть выше пунктира, потому что запрос стоит наложенной задержки плюс работы на обоих концах.

Здесь важен не сам факт «шесть запросов дольше одного», а то, во что превращается разница. Шесть последовательных обращений при 150 мс — это 972 мс: почти секунда, за которую на сервере ничего сложного не произошло. Тот же экран за одним обращением к BFF стоит одну задержку плюс работу внутри датацентра, где обращение к соседнему сервису обошлось в 0,71 мс по медиане двадцати замеров.

Отсюда — важная оговорка, о которой в пересказах обычно забывают. 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-сессии и сам подставляет нужный токен в запросы к ресурсному серверу.

Смысл конструкции черновик формулирует одной фразой: токены доступны только BFF, и в браузере извлекать нечего.

Это проверяемое утверждение, а не декларация. Ниже — измерение того, что достаёт скрипт, выполняющийся на странице, при трёх способах хранить сессию. Колонка «за BFF» — дословный вывод прогона против живого стенда (Next.js в продакшен-режиме перед Go-сервисом перед PostgreSQL); две другие сняты тем же способом на отдельной странице, куда тот же токен положили в localStorage и в куку без HttpOnly.

> localStorage.getItem("access_token")

Токен в localStorage

SPA ходит в API сама

утечка

"tok_1f3a9c8e2b7d4a06"

Тот самый токен, который был туда положен.

Токен в куке без HttpOnly

SPA ходит в API сама

недоступно

null

В localStorage его нет — он в куке.

Сессия в HttpOnly-куке за BFF

браузер знает только BFF

недоступно

null

Измерено: localStorage приложения пуст, ключей нет вообще.

Рис. 1. Значения в колонке «за BFF» — дословный вывод прогона против работающего стенда: Next.js в продакшен-режиме перед Go-сервисом перед PostgreSQL. Две другие колонки измерены тем же способом на отдельной странице, куда тот же токен положили в localStorage и в куку без HttpOnly. Обратите внимание на третью пробу: колонка «за BFF» отличается от остальных только тем, что у скрипта нет токена, — данные он получает всё равно.

Первые две пробы подтверждают ровно то, что обещано, и заодно объясняют, почему 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:

ТребованиеУровеньЧто это значит
SecureMUSTКука не уходит по открытому HTTP
HttpOnlyMUSTКуку не видно из document.cookie
SameSite=StrictSHOULDКука не прикладывается к кросс-сайтовым запросам вообще
Защита от CSRFMUSTОтдельный механизм, не заменяемый предыдущим пунктом

Последние две строки стоит развести, потому что их постоянно путают. 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-команды. Все три подхода отвечают на один вопрос, но по-разному распределяют, кто пишет код и кто за него отвечает.

Как это выглядит в этом приложении

Читатель этого сайта не ходит в Go-сервис напрямую: браузер знает только маршруты Next.js под /api, а токен сессии живёт в HttpOnly-куке, к которой у скриптов нет доступа. Модуль, из которого делаются исходящие вызовы, помечен server-only — импорт его из клиентского компонента ломает сборку, а не проходит ревью.

Это BFF ровно во втором смысле — том, который пришёл из черновика IETF. Агрегации здесь почти нет: маршруты в основном один к одному соответствуют вызовам Go-сервиса. По критерию Newman это как раз тот случай, когда BFF ради агрегации был бы не нужен, — и он здесь стоит не ради неё.

Что здесь стандарт, а что — решение команды

УтверждениеСтатус
Токен не должен лежать в localStorageТребование OWASP
Кука сессии — Secure и HttpOnlyMUST черновика IETF
SameSite=StrictSHOULD черновика IETF
Отдельная защита от CSRF обязательнаMUST черновика IETF
SameSite в одиночку от CSRF не защищаетПрямо сказано в rfc6265bis
«Один опыт — один BFF»Рекомендация Newman, не стандарт
BFF нужен веб-приложению только при заметной агрегацииОговорка Newman, не стандарт
Дублирование между BFF терпимоПозиция Newman, спорная
Lax вместо Strict в этом приложенииРешение команды, обосновано выше

Разделение здесь не формальность. Первые пять строк — то, за нарушение чего вам справедливо укажут на спецификацию. Следующие три — опыт конкретных людей, полученный на конкретных системах, и их можно оспаривать. Последняя — ваша собственная ответственность, и единственное, что делает её приемлемой, — записанная причина.

Что стоит помнить

Паттерн, у которого две несвязанные причины существования, опаснее обычного: легко внедрить его ради одной и решить, что получил обе. BFF, поставленный ради токенов, не ускорит экран сам по себе. BFF, поставленный ради агрегации, не защитит токен, если вы всё равно отдаёте его в JavaScript.

И ни один из них не остановит XSS — он лишь решает, что именно атакующий унесёт с собой, когда вкладка закроется.

Частые заблуждения

Утверждение

«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 оправдан, лишь если на сервере действительно есть заметная агрегация. Без неё вы получаете лишний сетевой прыжок и лишний деплой. Отдельная история — если слой нужен ради токенов: тогда это верное решение, но по другой причине, и называть его стоит честно.

Проверка знаний

Вопрос 1 из 5

В приложении сессия лежит в HttpOnly-куке за BFF. На странице сработала XSS. Что атакующий может сделать?

Источники и что читать дальше

8 ИСТОЧНИКОВ

  1. 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
  2. Sam Newman — Backends For FrontendsИсточник. Каноническое описание паттерна (первая публикация — 18 ноября 2015). Здесь собраны правило «one experience, one BFF» (Newman приводит его от Stewart Gleadow), наблюдение Pete Hodgson про границы команд и предупреждение о том, чем кончаются попытки слить BFF обратно в общий edge-сервис.https://samnewman.io/patterns/architectural/bff/
  3. 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
  4. Ben Christensen — Optimizing the Netflix APIИсточник. Netflix TechBlog, 15 января 2013. Продолжение той же истории: «2+ миллиарда входящих запросов в день» и объяснение, почему WAN-задержку выгодно платить один раз.https://netflixtechblog.com/optimizing-the-netflix-api-5c9ac715cf19
  5. 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
  6. 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
  7. 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
  8. OWASP — Session Management Cheat SheetОфициальная документация. Требования к идентификатору сессии: минимум 64 бита энтропии, флаги Secure и HttpOnly, прямой запрет хранить токены в localStorage и sessionStorage.https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html