Перетворення обробників маршрутів Next.js на свідомий шар BFF
Дізнайтеся, що вирішує паттерн Backend for Frontend, чому він знову стає популярним у додатках Next.js, та як уникнути перетворення обробників маршрутів на величезні «богові» об’єкти.
Один із розробників з мобільної команди колись запитав, чому для відображення екрана оформлення покупки потрібно було здійснити шість окремих викликів API, причому три з них повертали перекриваючіся поля, отримані з однієї й тієї самої базової служби. Чесна відповідь полягала у тому, що ніхто не визначав формату того, що насправді потребує фронтенд, тож кожна команда створювала власну версію рішення та залишала клієнта збирати все разом. Це насправді не технічна проблема, а організаційна, і саме для її вирішення була розроблена модель Backend for Frontend. Ймовірно, саме тому ця модель знову з’являється у кількох публікаціях останнім часом, після того як протягом десятиліття залишалась майже забутою ідеєю у технічних блогах Netflix та SoundCloud.
BFF зовсім не є новою ідеєю. Багато інженерів вже створювали подібні рішення під іншими назвами ще до того, як „BFF“ став стандартним терміном. Новизна полягає у тому, що цей підхід знову впроваджується у конкретному контексті додатків Next.js, які поступово перетворюються на шари агрегації API, часто без явного наміру з боку розробників. Враховуючи цю тенденцію, варто свідомо застосовувати цей паттерн, а не натрапити на нього випадково.
Форма проблеми
Ця ситуація постійно повторюється у трохи інших формах. У вас є кілька сервісів на стороні сервера: деякі розроблені власноруч, інші — це API від постачальників, додані пізніше. Також у вас є кілька клієнтів: веб-додаток, мобільний додаток та, можливо, внутрішній інструмент адміністрування. Кожен з цих клієнтів має свої вимоги. Мобільний клієнт потребує легкого об’єму даних, оскільки часто працює в ненадійній мережі. Веб-панель керування вимагає повністю денормалізованої відповіді з усіма даними, вже об’єднаними між собою, щоб уникнути послідовності запитів. Інструмент адміністрування потребує необроблених даних у первинному вигляді, оскільки люди, які ним користуються, зазвичай усувають проблеми.
Якщо пропустити шар BFF, залишаються два погані варіанти результату. Або ваші основні сервіси розростаються до величезних універсальних кінцевих точок, які намагаються задовольнити всіх можливих користувачів одночасно, що фактично перетворюється на місце поховання ідей типу „додай ще один необов’язковий параметр запиту“, або кожен клієнт змушений самостійно реалізовувати власну логіку агрегації на основі тих самих бекенд-сервісів. Таким чином у трьох окремих кодових базах з’являються три трохи різні та трохи неправильні визначення поняття „активний користувач“.
Що насправді є BFF
Бекенд для фронтенду — це легкий серверний шар, зазвичай один на кожен тип клієнта або групу клієнтів, який розташований між інтерфейсом користувача та справжніми бекенд-сервісами. Його єдиною функцією є обробка даних для конкретного користувача, якому він працює. BFF для вебу та BFF для мобільних пристроїв можуть викликати однакові базові сервіси, проте повертати абсолютно різну форму відповідей, оскільки кожен з них вирішує окрему проблему представлення даних.
Ця ідея постійно згадується під час обговорень у Next.js, оскільки App Router фактично надає вам шар BFF безкоштовно, за умови що ви будете його так використовувати. Обробники маршрутів та серверні компоненти вже виконуються на сервері, знаходяться між клієнтом та бекендом, і вже мають можливість надсилати кілька паралельних запитів та перетворювати об’єднаний результат ще до того, як він дістанеться браузеру. Багато команд, які перейшли на App Router, вже виконують завдання у стилі BFF, не називаючи їх формально чи не застосовуючи їх послідовно.
// app/api/checkout-summary/route.ts
// A web-specific BFF endpoint that aggregates three backend calls
// into the one shape the checkout screen actually needs
export async function GET(req: Request) {
const userId = getUserIdFromSession(req); const [cart, pricing, shippingOptions] = await Promise.all([
fetch(`${CART_SERVICE}/users/${userId}/cart`).then(r => r.json()),
fetch(`${PRICING_SERVICE}/quote?userId=${userId}`).then(r => r.json()),
fetch(`${SHIPPING_SERVICE}/options?userId=${userId}`).then(r => r.json()),
]); // Reshape into exactly what the checkout component renders,
// nothing more. This is the whole point.
return Response.json({
items: cart.items.map((i: CartItem) => ({ id: i.id, name: i.name, qty: i.qty })),
total: pricing.total,
currency: pricing.currency,
shipping: shippingOptions.filter((o: ShippingOption) => o.available),
});
}
Тепер уявіть, що мобільний додаток замість цього безпосередньо взаємодіє з цими трьома сервісами та самостійно обробляє агрегацію даних у Swift чи Kotlin, незалежно впроваджуючи логіку визначення доступних варіантів доставки. Саме так поступово з’являється розбіжність. Два команди, дві мови програмування, два різні тлумачення одного й того ж бізнес-правила, і рано чи пізно ці тлумачення починають суперечити одне одному, причому ніхто цього не помічає, поки на стіл хтось із співробітників не потрапить заява про підтримку.
Де справа стає політичною, а не лише технічною
Складна частина правильного впровадження BFF майже не пов’язана з написанням коду. Йдеться про те, хто є власником цього інструменту. BFF, створений лише для веб-команди, буде ефективним лише тоді, коли керувати ним буде особа з повноваженнями швидко вносити зміни — ідеально, це сама веб-команда, а не група на спільній платформі, обмежена двотижневим графіком випуску. Якщо шар BFF опиняється у власності окремої бекенд-команди, яка ставиться до нього як до ще одного мікросервісу, підпорядкованого тому ж процесу схвалення, що й усе інше, ви не вирішили початкову проблему — ви просто перемістили її на крок ближче до клієнта.
Саме тому ця схема так природно вписується у кодову базу Next.js: розробники, які пишуть обробники маршрутів, зазвичай є тими самими розробниками, які пишуть компоненти, що їх викликають. Не потрібно створювати заявки, немає передачі завдань між командами чи окремої процедури розгортання для координації. Якщо потрібно змінити формат відповіді з підсумком оплати, цю зміну роблять у тому самому pull request, що й для компонента, який її відображає.
Пастка: перетворення кожного обробника маршруту на „бога-об’єкт“
Існує ще один спосіб збою на протилежному кінці спектру, про який рідко згадують. Як тільки команда усвідомлює, що обробники маршрутів можуть об’єднувати все, що завгодно, виникає сильне бажання створити один величезний кінцевий пункт, який поверне всю початкову становище додатку за один виклик, адже менша кількість поїздок здається очевидною перевагою. Іноді це справді так. Але це також означає, що один обробник маршрутів починає підтримувати одночасно шість незалежних функцій, а зміна в модулі доставки змушує вас розміщувати її в тому самому файлі, що й віджет балів лояльності, оскільки обидва потрапляють у щось на кшталт getInitialAppState.
Ця модель залишається дійсною, коли кінцева точка BFF відповідає одному екрану або одній чітко визначеній функції, а не „усьому, що додаток може потребувати під час завантаження“. Якщо у вас виникають труднощі з найменуванням кінцевої точки відповідно до конкретної частини інтерфейсу, це зазвичай ознака того, що вона стала занадто широкою.
Є ще один пов’язаний попереджувальний сигнал, на який варто звернути увагу: кінцеві точки BFF, які поступово замість переробки даних починають повторно реалізовувати бізнес-логіку. Обробник маршрутів, який сам вирішує, чи підходить клієнт для безкоштовної доставки, замість того щоб просто запитати сервіс ціноутворення та передати відповідь, стає другим джерелом істини для правила, яке вже існує в іншій частині системи. Завданням BFF є переклад та об’єднання даних, а не приймання рішень. Як тільки він починає приймати рішення, ми повертаємося до тієї ж проблеми з відхиленням, про яку йшлося раніше, тільки вона знаходиться на один рівень ближче до клієнта, де її значно важче виявити під час перевірки коду.
Як я до цього підійшов
BFF не обов’язково потрібно розглядати як якусь формальну архітектурну зобов’язаність із діаграмою та сторінкою у wiki, хоча немає нічого поганого в цьому, якщо це відповідає способу роботи вашої організації. Для більшості команд, які вже працюють з Next.js, його прийняття — це скоріше питання надання назви тому, що вже існує: усвідомлення того, що ваші обробники маршрутів функціонують як BFF, відмова від ілюзії, що це звичайні API-маршрути, та структурування їх відповідно до реальних потреб клієнта, а не відповідно до меж бекенд-сервісів. Сама схема ніколи не була складною частиною. Складність полягає, і саме це затримує більшість команд, у тому, щоб визнати, що логіка агрегації потребує належного місця, а потім передати це місце команді фронтенду, яка насправді несе за це відповідальність, замість того, щоб чекати, поки екран оплати буде потребувати шести окремих викликів для відображення, перш ніж хтось зупиниться та запитає, чому.