Олег Марков
Деплой Feature-Sliced Design проекта - практическое руководство
Введение
Деплой проекта, построенного по подходу Feature-Sliced Design (FSD), на первый взгляд почти не отличается от развертывания обычного фронтенд-приложения. Но на практике архитектура FSD серьёзно влияет на то, как вы собираете проект, как прокидываете конфигурацию, как разделяете сборки по окружениям и как настраиваете CI/CD.
Давайте разберемся, какие особенности есть у деплоя FSD-проекта, на что стоит обратить внимание при настройке пайплайнов и конфигурации, и как избежать типичных проблем — от "сломанных" импортов слоёв до неправильных переменных окружения в проде.
В примерах я буду опираться на типичную связку:
- React-приложение на FSD
- Vite или Webpack как сборщик
- Docker для контейнеризации (опционально)
- GitHub Actions / GitLab CI как примеры CI/CD
Но общие идеи останутся полезными и в других связках.
Почему деплой FSD-проекта имеет особенности
Как архитектура влияет на процесс deployment
Feature-Sliced Design вводит несколько важных принципов:
- разделение кода на слои (app, processes, pages, widgets, features, entities, shared);
- строгая модульность и импорт только по разрешённым направлениям;
- часто — использование алиасов (например
@/shared,@/features).
На деплой это влияет так:
Сборка должна уважать алиасы слоёв.
Если при локальной разработке всё работает за счёт настроек Vite/Webpack/TS, на CI эти настройки должны быть полностью воспроизведены. Иначе при сборке вы получите ошибки "Module not found".Разные окружения могут включать/отключать фичи.
Часто FSD-проект использует фиче-флаги или конфигурацию на уровнеshared/configилиapp/config, и от окружения зависит, какие части включаются. Для этого нужны продуманные механизмы прокидывания переменных окружения.Несколько "входов" в приложение.
В FSD может быть несколько entry-пойнтов (например, для админки и публичной части), или разные процессы вprocesses. Это может потребовать настройки множественных сборок или роутинга на уровне сервера.Чёткая разделимость слоёв упрощает деплой нескольких фронтов.
Можно развернуть одну и ту же codebase с разной конфигурацией, отключая отдельные сущности или виджеты. Но тогда важно, чтобы процесс сборки был повторяемым и описан явно.
Теперь давайте пошагово посмотрим, как подготовить и реализовать deployment такого проекта.
Подготовка FSD-проекта к деплою
Базовая структура и точка входа
Обычно FSD-проект имеет структуру вида:
- app
- processes
- pages
- widgets
- features
- entities
- shared
Смотрите, я покажу вам пример entry-файла, который часто лежит в src/app/index.tsx или src/app/main.tsx:
// src/app/index.tsx
import React from "react"
import { createRoot } from "react-dom/client"
// Импорт корневого компонента приложения из слоя app
import { App } from "./App"
// Импорт глобальных стилей из shared или app
import "@/shared/styles/global.css"
const container = document.getElementById("root")
// Проверяем, что контейнер найден
if (!container) {
// В реальном проекте лучше кинуть ошибку или залогировать проблему
throw new Error("Root container not found")
}
// Создаем корень React и монтируем приложение
const root = createRoot(container)
root.render(<App />)
Важно, чтобы в конфигурации сборщика именно этот файл выступал entry-пойнтом.
Настройка алиасов для слоёв
В FSD огромное значение имеет удобный импорт по алиасам: @/shared, @/entities и т.д.
Для deployment важно, чтобы:
- сборщик понимал алиасы;
- TypeScript тоже понимал те же пути;
- тестовый раннер (Jest, Vitest) имел согласованную конфигурацию.
Пример настройки алиасов в Vite
Теперь давайте посмотрим на пример с Vite:
// vite.config.ts
import { defineConfig } from "vite"
import react from "@vitejs/plugin-react"
import path from "path"
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
// Здесь мы настраиваем пути для слоёв FSD
"@": path.resolve(__dirname, "src"),
"@app": path.resolve(__dirname, "src/app"),
"@pages": path.resolve(__dirname, "src/pages"),
"@widgets": path.resolve(__dirname, "src/widgets"),
"@features": path.resolve(__dirname, "src/features"),
"@entities": path.resolve(__dirname, "src/entities"),
"@shared": path.resolve(__dirname, "src/shared"),
},
},
})
Комментарии в коде помогут вам вспомнить, что эти алиасы должны совпадать со структурой проекта.
Настройка путей в TypeScript
Чтобы TypeScript понимал те же алиасы, добавьте paths в tsconfig.json:
{
"compilerOptions": {
"baseUrl": "src",
"paths": {
"@/*": ["*"],
"@app/*": ["app/*"],
"@pages/*": ["pages/*"],
"@widgets/*": ["widgets/*"],
"@features/*": ["features/*"],
"@entities/*": ["entities/*"],
"@shared/*": ["shared/*"]
}
}
}
Комментарии тут можно добавить в реальном файле, если вы используете tsconfig.base.json и отдельные конфиги для тестов.
Главное — следить за тем, чтобы конфигурации Vite/Webpack и TypeScript были синхронизированы. Иначе в CI сборка может проходить, а редактор — подсвечивать ошибки (или наоборот).
Конфигурация окружений для FSD-проекта
Где хранить конфигурацию в структуре FSD
Самый распространённый подход — хранить конфигурацию в shared/config или app/config.
Часто делают, например, так:
src/shared/config/env.ts— утилита для чтения переменных окружения;src/shared/config/app-config.ts— агрегированная конфигурация приложения;src/shared/config/feature-flags.ts— фиче-флаги.
Давайте разберемся на примере:
// src/shared/config/env.ts
// Здесь мы описываем тип окружения приложения
type AppEnv = "development" | "staging" | "production"
// Функция читает переменные окружения, которые подставит сборщик (Vite)
export const getEnv = () => {
// Vite подставит значения из process.env при сборке
const env = (import.meta.env.VITE_APP_ENV as AppEnv) || "development"
return {
env,
apiUrl: import.meta.env.VITE_API_URL as string,
// Можно добавить другие переменные по необходимости
sentryDsn: import.meta.env.VITE_SENTRY_DSN as string | undefined,
}
}
Комментарии показывают, как именно Vite подставит значения в момент сборки.
Использование конфигурации в слоях
Теперь давайте посмотрим, как использовать эту конфигурацию, например, в слое shared или entities:
// src/shared/api/base-api.ts
import axios from "axios"
import { getEnv } from "@/shared/config/env"
// Получаем конфигурацию окружения
const { apiUrl } = getEnv()
// Создаем базовый инстанс axios с нужным baseURL
export const apiClient = axios.create({
baseURL: apiUrl,
// Здесь же можно добавить таймауты, заголовки и т.д.
})
Такой подход помогает вам:
- иметь одну точку входа для API-конфигурации;
- легко переключать URL в зависимости от окружения;
- не тянуть
process.envнапрямую в каждый модуль.
Сборка FSD-проекта под разные окружения
Сценарии сборки
Обычно в package.json FSD-проекта есть несколько скриптов:
- сборка для продакшена;
- сборка для staging;
- сборка для dev (локальной разработки, но иногда отдельная сборка).
Пример:
{
"scripts": {
"dev": "vite",
"build": "vite build",
"build:staging": "vite build --mode staging",
"preview": "vite preview"
}
}
Здесь ключевой момент — использование --mode, которое позволяет Vite подхватывать разные файлы переменных окружения (.env, .env.production, .env.staging и т.д.).
Файлы .env и их связь с Feature-Sliced Design
В FSD-архитектуре конфиг приложения обычно не размазан по коду, а централизован. Поэтому важно, чтобы .env корректно подхватывался и прокидывался именно в слой shared/config.
Структура может быть такой:
.env— базовые переменные для локальной разработки;.env.production— переменные для продакшена;.env.staging— для стейджинга.
Пример .env.production:
VITE_APP_ENV=production
VITE_API_URL=https://api.example.com
VITE_SENTRY_DSN=https://key@o0.ingest.sentry.io/0
Комментарии в .env вы тоже можете добавлять, чтобы объяснить коллегам, за что отвечает каждая переменная.
Контейнеризация FSD-проекта
Когда имеет смысл использовать Docker
Если вы деплоите фронтенд:
- на Kubernetes;
- в среду, где нужен единый образ;
- или хотите стандартизировать окружение (Node, зависимости, build tools),
то контейнеризация FSD-проекта — удобный путь.
Архитектура FSD почти не меняет логику Dockerfile, но вы чаще будете:
- использовать мультистейдж-сборку (отдельный этап build, отдельный — serve);
- собирать проект однажды, а затем использовать артефакт в лёгком Nginx-образе.
Пример Dockerfile для FSD-приложения на Vite
Покажу вам, как это выглядит на практике:
# Этап 1 - сборка фронтенда
FROM node:20-alpine AS builder
# Устанавливаем рабочую директорию внутри контейнера
WORKDIR /app
# Копируем package.json и lock-файлы
COPY package*.json ./
# Устанавливаем зависимости
RUN npm ci
# Копируем весь исходный код проекта
COPY . .
# Устанавливаем переменную окружения для сборки
# Здесь можно использовать ARG и передавать значения при сборке
ARG VITE_API_URL
ENV VITE_API_URL=${VITE_API_URL}
# Запускаем сборку production-версии приложения
RUN npm run build
# Этап 2 - легковесный сервер для отдачи статики
FROM nginx:alpine
# Удаляем дефолтную конфигурацию nginx
RUN rm /etc/nginx/conf.d/default.conf
# Копируем нашу кастомную конфигурацию nginx
COPY ./.deploy/nginx.conf /etc/nginx/conf.d/default.conf
# Копируем собранные статические файлы из builder-образа
COPY --from=builder /app/dist /usr/share/nginx/html
# Указываем порт, который будет слушать контейнер
EXPOSE 80
# Стартуем nginx в форграунде
CMD ["nginx", "-g", "daemon off;"]
Комментарии в Dockerfile помогут вам понять, как каждый шаг связан с деплоем FSD-приложения.
Конфигурация nginx для SPA на FSD
Развертывание FSD-приложения в многодоменной архитектуре или с роутингом на стороне клиента обычно требует настройки fallback на index.html.
Вот пример простого конфига:
# .deploy/nginx.conf
server {
listen 80;
server_name _;
# Корень, куда мы скопировали статику
root /usr/share/nginx/html;
index index.html;
# Отдача статических файлов
location / {
try_files $uri $uri/ /index.html;
}
# Пример проброса запросов к API, если нужен прокси
location /api/ {
proxy_pass https://api.example.com;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Здесь FSD никак специально не "участвует", но важно настроить корректный try_files, чтобы роуты страниц (/profile, /dashboard) отдавали ваш SPA, а не 404.
Настройка CI/CD для FSD-проекта
Общая идея пайплайна
Типичный пайплайн деплоя для FSD-проекта:
- Проверка кода, линтеры, типы.
- Запуск тестов (юнит, e2e — по возможности).
- Сборка проекта (или Docker-образа).
- Деплой на выбранную платформу.
Строгость слоёв FSD и его структура упрощают автоматизацию: у вас обычно есть понятный src с фиксированными подпапками, а дополнительные проверки импортов (например, eslint-plugin для FSD) легко подключаются на шаге линтинга.
Пример GitHub Actions для сборки и деплоя
Смотрите, я покажу вам упрощённый пример workflow:
# .github/workflows/deploy.yml
name: Deploy FSD App
on:
push:
branches:
- main
jobs:
build-and-deploy:
runs-on: ubuntu-latest
env:
# Здесь мы прокидываем переменные окружения для сборки
VITE_APP_ENV: production
VITE_API_URL: https://api.example.com
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Run linters
run: npm run lint
- name: Run tests
run: npm test
- name: Build app
run: npm run build
# Здесь можно загрузить артефакты сборки
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: dist
path: dist
# Далее - шаг деплоя на ваш хостинг (S3, VPS, Kubernetes и т.д.)
# Я укажу пример для условного провайдера
- name: Deploy to hosting
run: |
# Здесь может быть скрипт загрузки dist на сервер
# Например, через rsync или CLI провайдера
echo "Deploy step goes here"
Комментарии показывают ключевые моменты:
- окружение для сборки (
env); - шаги проверки FSD-кода (линтеры, тесты);
- сборка и выгрузка артефактов.
Работа с переменными окружения в CI
Самая частая ошибка — пытаться использовать .env на CI "как есть". Лучше:
- хранить секретные значения (например,
VITE_SENTRY_DSN) в секретах CI; - подставлять их через
envв пайплайне; - не коммитить
.env.productionв репозиторий, если там чувствительные данные.
Пример с секретами GitHub Actions:
env:
VITE_APP_ENV: production
VITE_API_URL: ${{ secrets.VITE_API_URL }}
VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }}
Так вы не "зашиваете" реальные значения в коде, а FSD-конфиг (shared/config/env.ts) продолжает корректно работать, читая import.meta.env.
Deployment FSD-приложения на разные платформы
Статический хостинг (Netlify, Vercel, S3)
Для чисто фронтендового FSD-приложения чаще всего достаточно статики:
- Выполнить
npm run build. - Залить содержимое
dist(илиbuild) на хостинг. - Настроить правила для SPA (fallback на
index.html).
Особенности для FSD:
- Структура проекта не меняет деплой как статики.
- Но вам важно корректно настроить переменные окружения на самой платформе (например, в интерфейсе Netlify или Vercel).
Типичный сценарий для Netlify:
- команда сборки:
npm run build; - директория деплоя:
dist; - переменные окружения:
VITE_API_URL,VITE_APP_ENVи т.д.
Kubernetes и Docker-based платформа
Если вы используете Docker (как мы рассматривали выше), деплой часто выглядит так:
Сборка Docker-образа с указанием аргументов:
# Здесь мы передаем URL API и окружение docker build \ --build-arg VITE_API_URL=https://api.example.com \ -t registry.example.com/my-app:latest .- Публикация образа в registry.
- Обновление deployment в Kubernetes, который тянет новый образ.
Особенность FSD здесь:
- вы легко можете иметь несколько deployment-объектов, основанных на одном репозитории, но с разными
build-arg(например, для разных клиентов или white-label-схемы).
Организация конфигурации под несколько окружений и клиентов
Мультибрендовый или мультиклиентский деплой
FSD удобен, когда нужно:
- иметь единое ядро (
shared,entities,features); - и несколько "обёрток" для разных клиентов (бренды, кастомные страницы, уникальные виджеты).
Например:
app/client-a;app/client-b.
И каждая сборка может использовать свой entry-пойнт.
Пример мульти-entry в Vite
Теперь вы увидите, как это выглядит в конфиге:
// vite.config.ts
import { defineConfig } from "vite"
import react from "@vitejs/plugin-react"
import path from "path"
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
"@": path.resolve(__dirname, "src"),
},
},
build: {
rollupOptions: {
input: {
// Разные entry-пойнты для разных клиентов
clientA: path.resolve(__dirname, "src/app/client-a/index.html"),
clientB: path.resolve(__dirname, "src/app/client-b/index.html"),
},
},
},
})
Комментарии здесь объясняют, зачем несколько входов:
- вы можете собрать два набора статики в рамках одного deployment;
- на сервере их можно отдавать с разных поддоменов или путей.
Такой подход часто сочетается с разными .env или переменными окружения в CI, чтобы для каждого клиента указать свои URL API и прочие настройки.
Типичные ошибки при деплое FSD-проекта и как их избежать
Ошибка 1. Несогласованные алиасы в сборщике и TypeScript
Симптомы:
- локально всё работает, но при сборке на CI: "Module not found" для
@/shared; - или наоборот — редактор не находит модуль, а сборка проходит.
Как исправить:
- Проверить, что
vite.config.tsиtsconfig.jsonиспользуют одинаковые алиасы иbaseUrl. - При изменении структуры слоёв (например, добавлении
processes) обновлять оба файла. - В CI не использовать кастомный
tsconfigбез нужныхpaths.
Ошибка 2. Прямое использование process.env в коде
В проектах на FSD с Vite или CRA часто допускают смешение:
- где-то используют
import.meta.env; - где-то —
process.env.
В результат на деплое (особенно в Docker) значения могут не подставиться.
Как исправить:
- определиться, какой подход вы используете (для Vite —
import.meta.env); - сделать единый слой-конфиг (
shared/config/env.ts) и использовать только его; - включить линтер, который запрещает прямой доступ к
process.envвне этого слоя.
Ошибка 3. Захардкоженные URL и флаги по слоям
Иногда разработчики пишут:
// Плохой пример
const apiUrl = "https://api-dev.local"
Вместо чтения из конфигурации. В FSD-проекте это быстро становится проблемой, потому что:
- логику и конфигурацию сложно переиспользовать;
- деплой на несколько окружений требует ручного поиска "магических строк".
Как исправить:
- вынести все такие значения в
shared/config; - использовать переменные окружения только в одном месте;
- при необходимости — добавить модуль в
shared/configдля feature-flags.
Ошибка 4. Отсутствие SPA fallback на боевом сервере
Симптомы:
- главная страница открывается, но прямой переход на
/profileдаёт 404; - в dev-режиме всё работает.
Причина — сервер (nginx, Apache, CDN) не настроен корректно для Single Page Application.
Решение:
- добавить правило
try_files $uri $uri/ /index.html(для nginx); - или аналог для другого сервера;
- убедиться, что оно применимо ко всем роутам, которые обслуживаются SPA.
Заключение
Деплой Feature-Sliced Design проекта с технической точки зрения очень похож на обычный деплой фронтенда. Разница в том, что архитектурные принципы FSD подталкивают вас к:
- централизованной конфигурации (
shared/config,app/config); - аккуратному использованию алиасов и правильной настройке сборщика;
- более продуманной схеме окружений и фиче-флагов;
- чёткому разделению точек входа (особенно при мультиклиентских деплоях).
Если вы изначально заложите:
- единый модуль для чтения переменных окружения;
- согласованную конфигурацию Vite/Webpack, TypeScript и тестов;
- понятный CI-пайплайн с проверками и сборкой,
то дальнейший деплой FSD-проекта на любые платформы (от статического хостинга до Kubernetes) будет довольно прямолинейным. А сама архитектура FSD поможет масштабировать не только кодовую базу, но и вашу схему развертывания — под разные окружения, бренды, клиентов и фичи.
Частозадаваемые технические вопросы по теме и ответы
1. Как деплоить FSD-проект, если есть SSR или BFF-слой?
Обычно SSR/BFF выносится в отдельный пакет или директорию (/server), а фронтенд остаётся в FSD-структуре (/client/src). В этом случае:
- Соберите фронтенд как отдельный артефакт (например,
npm run buildв/client). - На сервере (Node/Next/Nest) подключите статику из
client/distили аналогичной папки. - Описывайте deployment как два контейнера (server и client) либо один мультистейдж-образ, где бандл фронта копируется внутрь серверного образа.
Важно не смешивать слои FSD с кодом сервера, чтобы не запутать imports и зависимость направлений.
2. Как организовать деплой, если нужно держать разные версии фич для разных пользователей?
Решение обычно строится на feature-flags:
- В
shared/config/feature-flags.tsзаводите флаги. - Источник значений делаете внешним — API, конфиг-сервис, LaunchDarkly и т.п.
- При деплое вы не меняете код, а переключаете флаги через конфигурацию окружения или внешний сервис.
- В CI/CD различия между окружениями задаются в переменных окружения и конфигурации флагов, а не в бранчах кода.
3. Как деплоить FSD-проект в монорепозитории с бэкендом и мобильными приложениями?
Обычно используют:
- Monorepo-менеджер (Nx, Turborepo, pnpm workspace).
- Отдельный пакет
web-appс FSD-структурой. - В CI настраивают таргетированные пайплайны — если меняется только
web-app, запускается только его сборка и деплой. - Артефакты фронта деплоятся независимо от бэкенда, но могут использовать общие версии API-конрактов (например, через shared-пакеты типов).
4. Как поступить, если FSD-проект должен деплоиться и как SPA, и как встроенный виджет?
Практика такая:
- В
appделаете два entry-пойнта — основной SPA и "микрофронт" (например,widget.tsx). - В сборщике настраиваете два entry (как в примере с мульти-entry).
- В CI/CD собираете один артефакт, но деплоите:
- SPA — как обычное приложение;
- bundle виджета — в отдельное место или как NPM-пакет для встраивания.
- На стороне потребителя (другой сайт) подключается только нужный bundle виджета.
5. Как лучше организовать версионирование и откаты релизов FSD-приложения?
Подход не уникален для FSD, но хорошо с ним сочетается:
- Версии приложения (например, в
package.json) метите тэгами в Git. - В CI каждое тегированное создание релиза инициирует сборку и деплой.
- Для отката деплоя используете:
- либо механизм версионирования артефактов (S3/CloudFront, Docker tags),
- либо деплой предыдущего Git-тэга.
- FSD упрощает это тем, что изменения обычно локализованы по слоям, и легче отследить, какие фичи попали в конкретный релиз.