React fullstack архитектура: как разделить UI, BFF, данные и рендеринг без хаоса

Разбираем React fullstack архитектуру на практике: BFF, SSR, RSC, server actions, state, performance, типичные ошибки и сильный ответ на интервью.

03 апреля 2026 г.19 минLexicon Team

Введение

Потребность в 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-стеке:

  1. Browser запрашивает маршрут.
  2. Edge или Node runtime проверяет сессию, cookies, регион, feature flags.
  3. Серверный слой агрегирует данные из backend API или доменных сервисов.
  4. Сервер рендерит статическую часть страницы и серверные компоненты.
  5. В браузер отправляется только тот JavaScript, который нужен интерактивным островам.
  6. Клиентские компоненты гидрируются и продолжают локальный сценарий пользователя.
  7. Мутации идут либо через 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.

Поток запроса

Для страницы каталога заказов хороший поток выглядит так:

  1. Сервер получает tenantId, role и query params.
  2. BFF делает один агрегирующий запрос или координирует несколько вызовов.
  3. Сервер возвращает уже нормализованные данные под экран.
  4. Клиент получает только таблицу, фильтры и контролы интерактивности.
  5. Изменение фильтра обновляет 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 архитектуры обычно строится так:

  1. Сначала определить тип продукта: публичные маршруты, закрытая SPA, mixed-mode.
  2. Потом показать границы: что рендерится на сервере, что остается клиенту, где источник истины.
  3. Отдельно назвать серверный orchestration layer: BFF, route handlers, server actions или API.
  4. Обязательно проговорить trade-off: меньше JS на клиенте в обмен на более сложный серверный контур.
  5. Завершить деградацией: что происходит при медленном 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

Читайте также