React fullstack архитектура: как разделить UI, BFF, данные и рендеринг без хаоса
Разбираем React fullstack архитектуру на практике: BFF, SSR, RSC, server actions, state, performance, типичные ошибки и сильный ответ на интервью.
- Введение
- Что входит в React fullstack архитектуру
- Клиентский слой
- Серверный слой рядом с UI
- Уровень рендеринга
- Базовая схема системы
- Архитектурный разбор: где проходят границы ответственности
- Контекст задачи
- Рабочее разделение
- Поток запроса
- Код: серверный read-path для первого экрана
- Код: BFF-слой, который агрегирует доменные данные
- Сравнение подходов
- Production pitfalls
- 1. Page-компонент превращается в контроллер
- 2. Серверное состояние дублируют в клиентском хранилище
- 3. Все мутации гонят через fetch из клиента
- Performance: где fullstack-архитектура реально выигрывает
- Что обычно становится bottleneck
- Когда оптимизация оправдана
- Когда это преждевременная оптимизация
- Best practices для React fullstack архитектуры
- Держите read-path и write-path раздельно
- BFF должен выдавать контракт экрана, а не проксировать backend как есть
- Клиентские компоненты должны быть маленькими и осознанными
- Правила зависимостей важнее структуры папок
- Частые ошибки
- Как отвечать на интервью
- FAQ
- React fullstack архитектура и Next.js это одно и то же?
- Нужен ли BFF, если backend уже отдает готовое API?
- Можно ли обойтись без глобального store?
- Где должны жить permissions?
- Когда fullstack-архитектура не нужна?
- Итоги
Введение
Потребность в React fullstack архитектуре обычно появляется в тот момент, когда команда перестает обсуждать только компоненты и хуки. Вопрос уже не в том, как собрать экран, а в том, где рендерить данные, кто отвечает за авторизацию, куда вынести агрегацию API, как не дублировать бизнес-логику между браузером и сервером и почему файл страницы неожиданно разрастается до нескольких сотен строк. На этой же границе часто возникает вопрос выбора платформы, который уже подробно рассмотрен в материале про Next.js vs чистый React.
Под React fullstack архитектурой обычно понимают не "все написано на React", а связку из нескольких уровней: клиентский UI, серверный слой для данных и мутаций, правила рендеринга, кеширования, авторизации и observability. Хорошая схема здесь ценна не красотой каталогов, а тем, что она уменьшает число произвольных решений. Если границы ясны, команда понимает, что должно жить в browser state, что лучше оставить серверу, а что не стоит тащить в клиентский bundle вообще.
Больше вопросов в Telegram
Ежедневные разборы и реальные кейсы с интервью.
Что входит в React fullstack архитектуру
Клиентский слой
За интерактивность на клиенте отвечают: формы, optimistic UI, локальное состояние, drag-and-drop, работа с фокусом, анимации, временные фильтры, open/close состояния и прочая логика, которая зависит от событий пользователя. Это обычный React, но уже встроенный в более широкую систему.
Серверный слой рядом с UI
Рядом появляется BFF или серверный слой уровня фреймворка. Его задачи:
- агрегировать ответы нескольких backend-сервисов;
- читать cookies, session и headers;
- скрывать приватные токены от браузера;
- выполнять server-side authorization;
- задавать правила кеша и revalidation;
- держать write-операции ближе к данным.
Именно этот слой обычно избавляет фронтенд от хаотичных цепочек из трех fetch подряд, ручную прокладку auth-заголовков и дублирование бизнес-правил в каждом экране.
Уровень рендеринга
В fullstack-архитектуре отдельно решают, где появляется первый полезный HTML и какая часть дерева вообще обязана гидрироваться в браузере. Здесь полезно понимать различия между SSR, CSR и RSC: это не просто набор модных аббревиатур, а реальная цена за latency, размер bundle и нагрузку на сервер.
Базовая схема системы
Ниже приведена рабочая схема для большинства production-проектов на React fullstack-стеке:
- Browser запрашивает маршрут.
- Edge или Node runtime проверяет сессию, cookies, регион, feature flags.
- Серверный слой агрегирует данные из backend API или доменных сервисов.
- Сервер рендерит статическую часть страницы и серверные компоненты.
- В браузер отправляется только тот JavaScript, который нужен интерактивным островам.
- Клиентские компоненты гидрируются и продолжают локальный сценарий пользователя.
- Мутации идут либо через API/BFF, либо через server actions, если платформа это поддерживает.
Смысл этой схемы в том, что браузер больше не обязан быть центром всей системы. Он отвечает за interaction, но не обязан первым узнавать все данные, принимать все архитектурные решения и хранить все состояние мира.
Архитектурный разбор: где проходят границы ответственности
Контекст задачи
Представим B2B-продукт: список заказов, фильтры, карточка клиента, роли доступа, экспорт, аудит действий и комментарии менеджеров. У проекта есть публичный landing с SEO и закрытая часть после логина. Если пытаться подходить ко всему одинаково через клиентский fetch, быстро появляются две проблемы: первый экран зависит от загрузки JS, а приватная логика начинает течь в браузер.
Рабочее разделение
server componentsили SSR-слой читают данные, которые нужны для первого экрана.client componentsобслуживают интерактивность, локальное состояние и быстрые пользовательские действия.BFFагрегирует backend и выдает удобный контракт именно под экран.domain servicesили backend остаются источником истины для правил бизнеса.cache layerхранит политику revalidate, dedupe и invalidation.
Главная проверка здесь простая: если удалить браузерный JavaScript, пользователь должен видеть осмысленный первый экран. Если да, значит read-path спроектирован неплохо. Если нет, значит система по-прежнему завязана на клиент как на главный runtime.
Поток запроса
Для страницы каталога заказов хороший поток выглядит так:
- Сервер получает
tenantId, role и query params. - BFF делает один агрегирующий запрос или координирует несколько вызовов.
- Сервер возвращает уже нормализованные данные под экран.
- Клиент получает только таблицу, фильтры и контролы интерактивности.
- Изменение фильтра обновляет URL и инициирует новый read-path без ручной синхронизации между пятью store.
Такой подход хорошо сочетается с идеей, что server state не надо без причины копировать в общий клиентский store. На практике это же хорошо показано в статье state management в React: часть проблем возникает не из-за библиотеки, а из-за неверного места для источника истины.
Код: серверный read-path для первого экрана
// app/orders/page.tsx
import { OrdersTable } from "./orders-table";
import { getSession } from "@/shared/auth/session";
import { getOrdersPageData } from "@/shared/server/get-orders-page-data";
type SearchParams = {
status?: string;
search?: string;
};
export default async function OrdersPage({
searchParams,
}: {
searchParams: Promise<SearchParams>;
}) {
const session = await getSession();
const params = await searchParams;
if (!session) {
throw new Error("Unauthorized");
}
const data = await getOrdersPageData({
tenantId: session.tenantId,
role: session.role,
filters: {
status: params.status ?? "all",
search: params.search ?? "",
},
});
return <OrdersTable initialData={data} />;
}
Здесь важен не синтаксис, а то, что page-компонент не знает деталей трех микросервисов, не собирает токены вручную и не превращается в слой бизнес-логики. Все сложное спрятано в серверной функции с явным контрактом.
Код: BFF-слой, который агрегирует доменные данные
// shared/server/get-orders-page-data.ts
type GetOrdersPageDataInput = {
tenantId: string;
role: "manager" | "admin" | "viewer";
filters: {
status: string;
search: string;
};
};
export async function getOrdersPageData(input: GetOrdersPageDataInput) {
const [orders, permissions, counters] = await Promise.all([
fetchOrders(input.tenantId, input.filters),
fetchPermissions(input.role),
fetchOrderCounters(input.tenantId),
]);
return {
rows: orders.items.map((order) => ({
id: order.id,
customer: order.customerName,
total: order.total,
status: order.status,
canEdit: permissions.canEditOrder,
})),
counters,
appliedFilters: input.filters,
};
}
Такой BFF полезен по двум причинам. Во-первых, браузер получает один понятный контракт вместо россыпи разноформатных ответов. Во-вторых, правила доступа остаются на сервере. Это важнее, чем кажется: если клиент сам вычисляет canEdit, ошибка быстро превращается в security-баг или в визуальную рассинхронизацию.
Сравнение подходов
| Подход | Где силен | Ограничения | Когда выбирать |
|---|---|---|---|
React SPA + отдельный API | Закрытые интерфейсы после логина, высокая интерактивность, простой deployment | Первый экран зависит от JS, больше client-side orchestration | Когда SEO и SSR не критичны |
React + BFF | Хорошо скрывает backend-сложность, нормализует контракты | Нужно поддерживать отдельный серверный слой | Когда фронтенд общается с несколькими сервисами |
Next.js / fullstack framework | Единая платформа для маршрутов, SSR, RSC, actions, кеша | Выше цена ошибок на границе client/server | Когда нужны публичные маршруты и server-first модель |
RSC-first чтение + client islands | Меньше клиентского JS, быстрый первый экран | Не вся логика подходит для серверных компонентов | Когда основная ценность в read-path |
Все через client fetch | Быстрый старт, знакомая модель | Разрастание bundle, auth и orchestration в браузере | Только для небольших и локальных сценариев |
Таблица полезна тем, что у fullstack-архитектуры нет единственного обязательного рецепта. Почти всегда это гибрид: часть маршрутов server-first, часть клиентские; часть мутаций идет через API, часть через actions; часть состояния живет в URL, а часть остается локальной в feature.
Прокачай React за 7 дней
20 вопросов и разборов по React Hooks.
Production pitfalls
1. Page-компонент превращается в контроллер
Симптомы:
- в
page.tsxлежит авторизация, нормализация данных, retry, mapping ошибок и JSX; - файл трудно тестировать отдельно;
- небольшое изменение API ломает экран и server logic одновременно.
Последствие в проде: растет время изменения маршрута, команды боятся рефакторинга, а ошибка в одном запросе валит весь экран. Исправление почти всегда одно: вынести оркестрацию (координацию) в серверный use case или BFF-функцию.
2. Серверное состояние дублируют в клиентском хранилище
Симптомы:
- после SSR данные снова записываются в Zustand/Redux "на всякий случай";
- появляются флаги
isHydrated,isLoadedOnce,didBootstrap; - invalidation размазывается между кэшем фреймворка и клиентским store.
Последствие: stale data, лишние ререндеры и сложная отладка. Если источник истины уже на сервере, не стоит делать второй без явной выгоды.
3. Все мутации гонят через fetch из клиента
Это выглядит просто, пока не начинаются audit-log, cookies, permissions, optimistic rollback и серверная revalidation. В этот момент write-path становится отдельной архитектурной задачей. Если проект использует server actions, стоит понимать их ограничения отдельно, как в разборе Server Actions в React: они удобны, но не отменяют необходимость явных доменных контрактов.
Performance: где fullstack-архитектура реально выигрывает
Самый частый выигрыш не в "React стал быстрее", а в том, что система меньше делает на клиенте.
Что обычно становится bottleneck
- тяжелый bundle для первого экрана;
- каскад клиентских
fetchпосле hydration; - повторная сериализация одних и тех же данных между слоями;
- избыточная гидрация больших деревьев;
- широкие client providers, которые подписывают на обновления полприложения.
Если сервер может отдать готовый HTML и убрать часть логики из клиентского runtime, вы выигрываете одновременно во времени до первого контента (TTFB + FCP), в размере бандла и в стабильности первого экрана. Но это не бесплатно: сервер получает дополнительную работу, а промахи в кешировании быстро съедают весь эффект.
Когда оптимизация оправдана
- p95 загрузки маршрута упирается в клиентский JS;
- Lighthouse и RUM показывают медленный first content на публичных страницах;
- read-path можно сделать серверным без потери UX;
- у команды есть дисциплина по кешу, revalidation и traceability.
Когда это преждевременная оптимизация
- почти весь продукт живет после логина и зависит от тяжелой интерактивности;
- bottleneck на самом деле в медленном backend API;
- у команды пока нет ресурсов поддерживать более сложный серверный контур;
- выигрыш от SSR меньше, чем цена инфраструктуры и отладки.
Когда нужно понять, действительно ли проблема в React-слое, а не в сетях и layout thrashing, помогает отдельный разбор про React performance profiling.
Best practices для React fullstack архитектуры
Держите read-path и write-path раздельно
Чтение данных и мутации редко живут по одинаковым правилам. Для чтения важны кеш, SSR, RSC и повторное использование результата. Для записи важны идемпотентность, авторизация, валидация и rollback. Если объединить оба сценария в один универсальный helper, он быстро становится неуправляемым.
BFF должен выдавать контракт экрана, а не проксировать backend как есть
Если серверный слой просто пробрасывает ответы один в один, фронтенд все равно остается интеграционным центром. Нормализация, permission flags, aggregates и композиция ответа должны происходить до попадания данных в UI.
Клиентские компоненты должны быть маленькими и осознанными
Чем уже зона гидрации, тем легче контролировать bundle и обновления. Здесь пересекается тема React Server Components: их сила не в магии, а в том, что они выталкивают часть read-only логики с клиента.
Правила зависимостей важнее структуры папок
Можно взять FSD, модульную архитектуру или свою схему именования. Но если features импортируют друг друга хаотично, а shared-слой уже содержит бизнес-логику, схема распадается. В этом смысле полезно смотреть на Feature-Sliced Design в React-проектах как на набор ограничений, а не на набор каталогов.
Частые ошибки
- Пытаться сделать fullstack-архитектуру только ради модного стека, без явной пользы для read-path или auth.
- Считать, что SSR автоматически решает performance-проблемы, не измеряя backend latency и стоимость hydration.
- Хранить на клиенте данные, которые уже являются серверным источником истины.
- Путать BFF с backend-for-everything и тащить в него доменную логику, которой место в сервисах.
- Делать универсальный
apiClient, который знает и браузер, и сервер, и куки, и ретраи, и все типы мутаций сразу.
Как отвечать на интервью
Сильный ответ на тему React fullstack архитектуры обычно строится так:
- Сначала определить тип продукта: публичные маршруты, закрытая SPA, mixed-mode.
- Потом показать границы: что рендерится на сервере, что остается клиенту, где источник истины.
- Отдельно назвать серверный orchestration layer: BFF, route handlers, server actions или API.
- Обязательно проговорить trade-off: меньше JS на клиенте в обмен на более сложный серверный контур.
- Завершить деградацией: что происходит при медленном API, ошибке авторизации, invalidation и partial failure.
Типичная формулировка звучит примерно так: "Я рассматриваю React fullstack архитектуру как систему из client UI, серверного read/write слоя и правил рендеринга. Сервер должен забирать то, что важно для первого экрана, авторизации и агрегации данных, а клиент должен обслуживать интерактивность. Если весь продукт живет после логина и SSR мало что дает, я не буду усложнять стек только ради fullstack-названия".
Такой ответ хорош тем, что в нем есть не только инструменты, но и критерии выбора. На интервью это почти всегда ценнее, чем перечисление возможностей конкретного фреймворка.
Потренируйте React-архитектуру и fullstack trade-off на реальных интервью-сценариях
Платформа помогает разбирать вопросы по React, Next.js, SSR, state management, BFF и performance так, как их обсуждают на технических собеседованиях и в реальных production-командах.
FAQ
React fullstack архитектура и Next.js это одно и то же?
Нет. На практике Next.js часто является платформой для такого подхода, но сама архитектурная идея шире: нужен серверный слой рядом с UI, а не обязательно конкретный фреймворк.
Нужен ли BFF, если backend уже отдает готовое API?
Не всегда. Если backend уже выдает экранные контракты, поддерживает auth-контекст и не заставляет клиент сшивать несколько ответов, отдельный BFF может быть лишним. Но в большинстве больших интерфейсов он появляется как способ локализовать интеграционную сложность.
Можно ли обойтись без глобального store?
Да, и часто это полезно. Если server state живет на сервере, URL хранит навигационное состояние, а локальный UI-state остается внутри feature, глобальный store нужен заметно реже, чем кажется в начале проекта.
Где должны жить permissions?
Источник истины для permissions должен быть на сервере. На клиент можно отдавать уже вычисленные capability-флаги для конкретного экрана, но не переносить туда окончательное принятие решения.
Когда fullstack-архитектура не нужна?
Когда проект почти целиком клиентский, работает после логина, не зависит от SEO и не выигрывает от server-first read-path. В таком случае можно получить больше сложности, чем пользы.
Итоги
React fullstack архитектура полезна не потому, что совмещает фронтенд и бэкенд в одном репозитории. Ее ценность в другом: она заставляет явно решить, кто читает данные первым, кто отвечает за auth и orchestration, сколько JavaScript реально нужно браузеру и где проходит источник истины.
Хорошая схема здесь почти всегда гибридная. Сервер берет на себя первый экран, агрегацию и доступ к защищенным данным. Клиент отвечает за interaction и локальный UX. BFF или framework-level слой связывает эти части, не давая page-компонентам превратиться в хаотичный центр всей системы. Если держать эти границы жестко, fullstack-подход действительно ускоряет продукт. Если нет, он просто меняет место, где находится архитектурный беспорядок.
Больше вопросов в Telegram
Ежедневные разборы и реальные кейсы с интервью.
Автор
Lexicon Team
Читайте также
frontend
Next.js vs чистый React: что выбрать под проект и как объяснить выбор на интервью
Сравниваем Next.js и чистый React: архитектура, SSR и RSC, производительность, стоимость разработки, типичные ошибки и критерии выбора для production.
frontend
Когда нужен Next.js: признаки, что фреймворк окупится в проекте
Разбираем, когда нужен Next.js на практике: SEO, SSR, RSC, серверные мутации, стоимость инфраструктуры, типичные ошибки выбора и сильный ответ на интервью.
frontend
Hydration в React: что происходит после SSR и где ломается интерактивность
Разбираем hydration в React после SSR: как браузер связывает HTML с деревом React, почему возникают hydration mismatch, сколько стоит гидрация и как уменьшить клиентскую нагрузку.