- Переработан раздел Workflow: заголовки, описания, порядок разделов - Добавлены новые разделы: Генерация кода (workflow), Настройка VS Code (applied) - Убран суффикс .ui.tsx из документации и примеров - Переработан раздел Структура проекта — только Next.js, без React SPA - Приоритет стилизации перенесён из applied/styles в workflow/styling - Убрано дублирование инструментов генерации — единая точка в applied/templates-generation - Переписан concat-md.js: без внешних зависимостей, мета-якоря для навигации в RULES.md - Удалена зависимость concat-md - Обновлена главная страница: названия разделов, URL на RULES.md - Добавлен AGENTS.md с правилами для агентов
67 KiB
NextJS Style Guide
Правила и стандарты разработки на NextJS и TypeScript: архитектура, типизация, стили, компоненты, API и инфраструктурные разделы.
Для ассистентов
Полная документация в одном MD файле: https://gromlab.ru/docs/nextjs-style-guide/raw/branch/main/generated/ru/RULES.md
Структура документации
Workflow
Что делать и в каком порядке — пошаговые инструкции.
| Раздел | Отвечает на вопрос |
|---|---|
| Начало работы | Что нужно знать перед началом разработки? |
| Создание проекта | Как начать новый проект? |
| Генерация кода | Какие модули должны генерироваться из шаблонов? |
| Добавление страницы | Как добавить новую страницу в проект? |
| Добавление UI-модуля | Как создать компонент, фичу, виджет, сущность или layout? |
| Стилизация | Как стилизовать компоненты в проекте? |
| Получение данных | Как получать данные с сервера? |
| Управление состоянием | Как работать с состоянием? |
| Локализация | Как добавлять переводы и подключать локализацию? |
Базовые правила
Каким должен быть код — стандарты, не привязанные к конкретной технологии.
| Раздел | Отвечает на вопрос |
|---|---|
| Технологии и библиотеки | Какой стек используем? |
| Архитектура | Как устроены слои FSD, зависимости, публичный API? |
| Стиль кода | Как оформлять код: отступы, кавычки, импорты, early return? |
| Именование | Как называть файлы, переменные, компоненты, хуки? |
| Документирование | Как писать JSDoc: что документировать, а что нет? |
| Типизация | Как типизировать: type vs interface, any/unknown, FC? |
Прикладные разделы
Как это настроить и использовать — конфигурация, структура и примеры кода для конкретных областей.
| Раздел | Отвечает на вопрос |
|---|---|
| Настройка VS Code | Как настроить редактор для проекта? |
| Структура проекта | Как организованы папки и файлы по FSD? |
| Компоненты | Как устроен компонент: файлы, пропсы, clsx, FC? |
| Page-level компоненты | Как описывать layout, page, loading, error, not-found? |
| Шаблоны и генерация кода | Как работают шаблоны, синтаксис и инструменты генерации? |
| Стили | Как писать CSS: PostCSS Modules, вложенность, медиа, токены? |
| Изображения | (не заполнен) |
| SVG-спрайты | (не заполнен) |
| Видео | (не заполнен) |
| API | (не заполнен) |
| Stores | (не заполнен) |
| Хуки | (не заполнен) |
| Шрифты | (не заполнен) |
| Локализация | (не заполнен) |
Начало работы
Что нужно знать перед началом разработки в проекте.
Стек проекта
Next.js (App Router), Mantine, Zustand, FSD.
Подробнее — Технологии и библиотеки.
Ключевые особенности
- Генерация вместо ручного создания — компоненты, фичи, виджеты, сторы и другие модули не создаются вручную. Файловая структура генерируется из шаблонов
.templates/. Ручное создание файловой структуры модулей запрещено. - Biome вместо ESLint + Prettier — один инструмент для линтинга и форматирования. Автофикс и сортировка импортов происходят автоматически при сохранении файла.
Настройка окружения
Открыть проект в VS Code и установить рекомендуемые расширения — редактор предложит это автоматически. Подробнее — Настройка VS Code.
Создание проекта
Как начать новый проект, соответствующий стандартам этого руководства.
Что нужно знать
Новый проект создаётся из готового шаблона. Шаблон содержит настроенный стек, структуру FSD, конфигурацию редактора и шаблоны генерации кода — проект готов к разработке сразу после установки зависимостей.
Создание из шаблона
npx tiged git@gromlab.ru:templates/nextjs.git my-app
cd my-app
npm install
Что входит в шаблон
- Next.js + TypeScript (App Router)
- Mantine UI + PostCSS Modules
- Biome (линтинг и форматирование)
- Zustand, SWR
- Структура FSD (
screens/,widgets/,features/,entities/,shared/) - Шаблоны генерации (
.templates/) - Конфигурация VS Code (
.vscode/) - CSS-токены (цвета, отступы, радиусы, медиа)
- Open Graph метаданные
Генерация кода
Как создавать модули в проекте с помощью шаблонов — какие модули покрыты генерацией и когда стоит создавать новые шаблоны.
Какие модули генерируются из шаблонов
| Модуль | Слой | Шаблон |
|---|---|---|
| Компонент | shared/ui/ |
component |
| Фича | features/ |
feature |
| Виджет | widgets/ |
widget |
| Сущность | entities/ |
entity |
| Layout | layouts/ |
layout |
| Экран | screens/ |
screen |
| Стор | model/ |
store |
Что нужно знать
В проекте принято создавать модули из шаблонов .templates/. Шаблоны задают единообразную файловую структуру и сокращают рутину — не нужно вручную создавать папки, файлы типов, стилей и экспорты.
Если для нужного модуля нет подходящего шаблона — стоит сначала создать шаблон, а затем использовать его.
Когда создавать новый шаблон
- Повторяющаяся структура появляется больше одного раза.
- Существующий шаблон не покрывает нужный тип модуля.
Инструменты и синтаксис шаблонов — Шаблоны и генерация кода.
Добавление страницы
Как добавить новую страницу в проект по стандартам этого руководства.
Что нужно знать
Страница в проекте — это два файла: экран в src/screens/ (вся логика, стили, зависимости) и page.tsx в src/app/ (точка входа для роутинга Next.js). Экран генерируется из шаблона, page.tsx создаётся вручную.
Порядок действий
-
Сгенерировать экран из шаблона
screenв папкуsrc/screens/. -
Заполнить экран логикой и стилями.
-
Создать
page.tsxв нужном маршрутеsrc/app/. Файл страницы должен быть тонким — толькоmetadataи рендер экрана. Никакой логики, стилей и хуков вpage.tsxне размещается — всё это живёт в экране.
Правила
- Ручное создание файловой структуры экрана запрещено — только генерация из шаблона.
- Логика, стили и зависимости размещаются в экране, не в
page.tsx. - Каждая страница содержит
metadataсtitleиdescription.
Примеры page.tsx и metadata — Page-level компоненты.
Добавление UI-модуля
Как создать компонент, фичу, виджет, сущность или layout в проекте.
Что нужно знать
Все UI-модули создаются только из шаблонов .templates/. Ручное создание файловой структуры запрещено. Если подходящего шаблона нет — сначала создать шаблон в .templates/, затем использовать его.
Порядок действий
- Сгенерировать модуль из соответствующего шаблона в целевой слой.
- Заполнить модуль логикой и стилями.
Дочерние компоненты
Если модулю нужны внутренние подкомпоненты — генерировать их из шаблона component в папку ui/ внутри родительского модуля. Дочерние компоненты не экспортируются через index.ts родителя.
Правила написания компонентов — Компоненты.
Стилизация
Как стилизовать компоненты в проекте — приоритет инструментов и правила их применения.
Приоритет стилизации
Основной UI-фреймворк проекта — Mantine. При стилизации компонентов придерживаться следующего приоритета:
- Mantine-компоненты и их пропсы — в первую очередь использовать встроенные возможности Mantine (пропсы,
classNames,styles). - Глобальные CSS-токены (
--color-*,--space-*,--radius-*) — для значений, которые не покрываются Mantine. - PostCSS Modules — когда Mantine не покрывает задачу и нужна кастомная стилизация.
Что запрещено
- Инлайн-стили — использование атрибута
styleв компонентах строго запрещено. - Магические значения — произвольные цвета, отступы и скругления запрещены, использовать токены.
- Глобальные стили вне
app/styles/запрещены.
Правила написания CSS, вложенность, медиа-запросы и токены — Стили.
Получение данных
Как получать данные с сервера — SWR, генерация API-клиентов, сокеты.
Управление состоянием
Как работать с состоянием — когда создавать стор, что хранить локально и глобально.
Локализация
Как добавлять переводы и подключать локализацию через i18next.
Технологии и библиотеки
Базовый стек технологий и библиотек, на который опираются проекты и примеры в документации.
Что используем
Стек
- React/TypeScript — основной стек для UI и приложения.
- Next.js — для продуктовых сайтов.
Архитектура
- FSD (Feature-Sliced Design) — структура проекта и границы модулей.
UI компоненты
- Mantine UI — базовые UI-компоненты.
Fetch (API)
- @gromlab/api-codegen — генерация API‑клиентов и типов.
- SWR — получение, кеширование, ревалидация, дедубликация.
- SWR (useSWRSubscription) - сокеты, реалтайм подписки.
Store
- Zustand — глобальное состояние.
Локализация
- i18next (i18n) — локализация всех пользовательских текстов.
Тестирование
- Vitest — тестирование.
Стили
- PostCSS Modules — изоляция стилей.
- Mobile First — подход к адаптивной верстке.
- clsx — конкатенация CSS‑классов.
Генерация
- @gromlab/create — шаблонизатор для создания слоёв и других файлов из шаблонов.
Архитектура
Архитектура построена на FSD (Feature‑Sliced Design) и строгих границах модулей.
Цель — разделить ответственность, упростить сопровождение и контроль зависимостей.
Принципы
- Разделять UI, бизнес-логику и инфраструктуру.
- Держать зависимости однонаправленными.
- Открывать наружу только публичный API модулей.
- Не допускать циклических зависимостей.
Слои (FSD)
- app — инициализация приложения: провайдеры, глобальные стили. В Next.js эта же папка
app/дополнительно содержит системные файлы роутинга (layout.tsx,page.tsx). - screens — UI-компоненты страниц. Каждый экран — отдельный компонент, который собирает виджеты и фичи конкретной страницы. Роутинг только использует эти компоненты — он не является частью слоя
screens. В Next.js файлыpage.tsxостаются тонкими: импортируют экран и рендерят его. - layouts — каркас и шаблоны страниц.
- widgets — крупные блоки интерфейса, собирающие несколько сценариев.
- features — отдельные пользовательские действия и сценарии.
- entities — бизнес-сущности и их модель.
- shared — переиспользуемая инфраструктура, утилиты и базовые UI‑компоненты.
Модули (FSD)
- Модуль — это отдельная папка в слоях
screens,layouts,widgets,features,entities, которая реализует один сценарий/блок. В корне модуля лежит главный файл (*.screen.tsx,*.layout.tsx,*.widget.tsx,*.feature.tsx,*.entity.tsx) и публичный API (index.ts). - Внутри модуля используются подпапки (по необходимости):
ui/— дочерние UI‑компоненты модуля.model/— состояние и бизнес‑логика модуля.styles/— локальные стили модуля.helpers/— локальные хелперы.lib/— утилиты модуля.api/— API‑вызовы модуля.
Правила зависимостей
- Допустимые импорты идут сверху вниз:
app → screens → layouts → widgets → features → entities → shared. - Импорты между слоями — через публичный API.
- Внутри одного слоя — относительные импорты.
Публичный API модулей
- Каждый модуль экспортирует наружу только то, что нужно другим слоям.
- Внешние импорты идут только через
index‑файл модуля. - Внутренние файлы не импортируются напрямую извне.
Границы ответственности
- Бизнес‑логика не размещается в UI‑компонентах.
- UI‑компоненты должны быть максимально простыми и предсказуемыми.
- Связь между независимыми сценариями поднимается на уровень выше.
Типовые ошибки
- Импорт из более высокого слоя в более низкий.
- Смешивание логики нескольких слоёв в одном модуле.
- Прямые импорты внутренних файлов, минуя публичный API.
Стиль кода
Раздел описывает единые правила оформления кода: отступы, переносы, кавычки, порядок импортов и базовую читаемость.
Отступы
- 2 пробела (не табы).
Длина строк
- Ориентироваться на 100 символов, но превышение допустимо, если строка читается легко.
- Переносить выражение на новые строки, когда строка становится плохо читаемой.
- Не переносить строку внутри строковых литералов без необходимости.
Хорошо
const config = createRequestConfig(
endpoint,
{
headers: {
'X-Request-Id': requestId,
'X-User-Id': userId,
},
params: {
page,
pageSize,
sort: 'createdAt',
},
},
timeoutMs,
);
Плохо
// Плохо: длинная строка с вложенными структурами плохо читается.
const config = createRequestConfig(endpoint, { headers: { 'X-Request-Id': requestId, 'X-User-Id': userId }, params: { page, pageSize, sort: 'createdAt' } }, timeoutMs);
Кавычки
- В JavaScript/TypeScript использовать одинарные кавычки.
- В JSX/TSX для атрибутов использовать двойные кавычки.
- Шаблонные строки использовать только при интерполяции или многострочном тексте.
Хорошо
const label = 'Сохранить';
const title = `Привет, ${name}`;
<input type="text" placeholder="Введите имя" />
Плохо
// Плохо: двойные кавычки в TS и конкатенация вместо шаблонной строки.
const label = "Сохранить";
const title = 'Привет, ' + name;
// Плохо: одинарные кавычки в JSX-атрибутах.
<input type='text' placeholder='Введите имя' />
Точки с запятой и запятые
- Допускаются упущения точки с запятой, если код остаётся читаемым и однозначным.
- В многострочных массивах, объектах и параметрах функции запятая в конце допускается, но не обязательна.
Импорты
- В именованных импортах использовать пробелы внутри фигурных скобок.
- Типы импортировать через
import type. defaultэкспорт избегать, использовать именованные.defaultимпорт допустим (например, стили CSS Modules, сторонние библиотеки).- Избегать импорта всего модуля через
*.
Хорошо
import { MyComponent } from 'MyComponent';
import type { User } from '../model/types';
import styles from './styles/button.module.css';
Плохо
// Плохо: отсутствие пробелов в именованном импорте.
import type {User} from '../model/types';
// Плохо: default экспорт.
export default MyComponent;
Ранние возвраты (early return)
- Использовать ранние возвраты для упрощения чтения.
- Избегать
elseпослеreturn.
Хорошо
const getName = (user?: { name: string }) => {
if (!user) {
return 'Гость';
}
return user.name;
};
Плохо
// Плохо: лишний else после return усложняет чтение.
const getName = (user?: { name: string }) => {
if (user) {
return user.name;
} else {
return 'Гость';
}
};
Форматирование объектов и массивов
- В многострочных объектах каждое свойство на новой строке.
- В многострочных массивах каждый элемент на новой строке.
- Объекты и массивы можно писать в одну строку, если длина строки не превышает 100 символов.
- В однострочных объектах и массивах использовать пробелы после запятых.
Хорошо
const roles = ['admin', 'editor', 'viewer'];
const options = { id: 1, name: 'User' };
const config = {
url: '/api/users',
method: 'GET',
params: { page: 1, pageSize: 20 },
};
Плохо
// Плохо: нет пробелов после запятых и объект слишком длинный для одной строки.
const roles = ['admin','editor','viewer'];
const options = { id: 1,name: 'User' };
const config = { url: '/api/users', method: 'GET', params: { page: 1, pageSize: 20 } };
Именование
Именование должно быть предсказуемым, коротким и отражать смысл сущности.
Базовые правила
| Что | Рекомендуется |
|---|---|
| Папки | kebab-case |
| Файлы | kebab-case |
| Переменные | camelCase |
| Константы | SCREAMING_SNAKE_CASE |
| Классы | PascalCase |
| React-компоненты | PascalCase |
| Хуки | useSomething |
| CSS классы | camelCase |
Архитектурный неймспейс
Соглашение о суффиксах, которые обозначают слой (уровень абстракции), роль или тип файла.
- Суффиксы используются для обозначения слоя, роли или типа файла.
- Суффиксы всегда пишутся в единственном числе.
- Формат имени:
name.<suffix>.tsилиname.<suffix>.tsx.
UI и слои FSD
.screen.tsx— экран.layout.tsx— layout.widget.tsx— виджет.feature.tsx— UI фичи.entity.tsx— UI сущности
Остальные .tsx файлы (компоненты в shared/ui/, дочерние компоненты в ui/) не помечаются суффиксами — расширение .tsx само по себе означает UI‑компонент.
Логика и модель
.store.ts— стор.service.ts— сервис
Типы и контракты
.type.ts— типы и интерфейсы.interface.ts— файл с интерфейсами (если нужен отдельный контракт).enum.ts— enum.dto.ts— внешние DTO.schema.ts— схемы валидации.constant.ts— константы.config.ts— конфигурация
Утилиты и хелперы
.util.ts— утилиты.helper.ts— вспомогательные функции.lib.ts— вспомогательные функции
Тесты
.test.ts/.test.tsx.mock.ts
Хорошо
src/
├── screens/
│ └── main/
│ ├── main.screen.tsx
│ └── index.ts
├── features/
│ └── auth-by-email/
│ ├── ui/
│ │ └── login-form.tsx
│ ├── auth-by-email.feature.tsx
│ └── index.ts
└── shared/
└── ui/
└── icon/
├── styles/
│ └── icon.module.css
├── types/
│ └── icon.interface.ts
├── icon.tsx
└── index.ts
Плохо
// Плохо: нет единых правил для слоёв и публичных файлов.
src/
├── screens/
│ └── Main/
│ └── Main.tsx
└── features/
└── authByEmail/
└── login-form.tsx
Булевы значения
- Использовать префиксы
is,has,can,should.
Хорошо
const isReady = true;
const hasAccess = false;
const canSubmit = true;
const shouldRedirect = false;
Плохо
// Плохо: неясное булево значение без префикса.
const ready = true;
const access = false;
const submit = true;
События и обработчики
- Обработчики начинать с
handle. - События и колбэки начинать с
on.
Хорошо
const handleSubmit = () => { ... };
const onSubmit = () => { ... };
Плохо
// Плохо: неочевидное назначение имени.
const submitClick = () => { ... };
Коллекции
- Для массивов использовать имена во множественном числе.
- Для словарей/мап — использовать суффиксы
ById,Map,Dict.
Хорошо
const users = [];
const usersById = {} as Record<string, User>;
const userIds = ['u1', 'u2'];
const ordersMap = new Map<string, Order>();
const featureFlagsDict = { beta: true, legacy: false } as Record<string, boolean>;
Плохо
// Плохо: имя не отражает, что это коллекция.
const user = [];
// Плохо: словарь назван как массив.
const usersMap = [];
// Плохо: по имени непонятно, что это словарь.
const users = {} as Record<string, User>;
Документирование
Документирование должно помогать понять назначение сущности, а не дублировать её типы или очевидные детали.
Правила
- Документировать только назначение функций, компонентов, типов, интерфейсов и enum.
- Не документировать параметры, возвращаемые значения, типы пропсов и очевидные детали.
- В интерфейсах, типах и enum описывать только смысл поля или значения.
- Описание должно быть кратким, информативным и завершаться точкой.
Примеры
Хорошо
/**
* Список задач пользователя.
*/
export const TodoList = memo(() => { ... });
/**
* Интерфейс задачи.
*/
export interface TodoItem {
/** Уникальный идентификатор задачи. */
id: string;
/** Текст задачи. */
text: string;
/** Статус выполнения задачи. */
completed: boolean;
}
/**
* Перечисление фильтров задач.
*/
export enum TodoFilter {
/** Все задачи. */
All = 'all',
/** Только активные задачи. */
Active = 'active',
/** Только выполненные задачи. */
Completed = 'completed',
}
Плохо
// Плохо: дублирование параметров и возвращаемых значений.
/**
* @param id - идентификатор задачи
* @returns объект задачи
*/
// Плохо: описание очевидных деталей.
/**
* id — идентификатор задачи
* text — текст задачи
* completed — статус выполнения
*/
Типизация
Типизация обязательна для всех публичных интерфейсов, функций и компонентов. Цель — предсказуемость, безопасность и автодополнение.
Общие правила
- Указывать типы для параметров компонентов, возвращаемых значений и параметров функций.
- Предпочитать
typeдля описания сущностей иinterfaceдля расширяемых контрактов. - Избегать
anyиunknownбез необходимости. - Не использовать
ts-ignore, кроме крайних случаев с явным комментарием причины.
Типы для компонентов
- Типизировать параметры и публичный интерфейс компонента.
- Дефолтные значения описывать явно в коде.
Хорошо
/**
* Параметры кнопки.
*/
interface ButtonProps extends HTMLAttributes<HTMLDivElement> {
/** Текст кнопки. */
label: string;
/** Обработчик клика по кнопке. */
onClick: () => void;
}
/**
* Кнопка с пользовательскими стилями.
*/
export const Button: FC<ButtonProps> = ({ className, label, onClick, ...htmlAttr }) => {
return (
<div {...htmlAttr} className={cl(styles.root, className)}>
button
</div>
)
}
Плохо
// Плохо: параметры не типизированы.
export const Button = (props) => (
<button type="button" onClick={props.onClick}>
{props.label}
</button>
);
Функции
- Для публичных функций указывать возвращаемый тип.
- Не полагаться на неявный вывод для важных API.
Хорошо
export const formatPrice = (value: number): string => {
return `${value} ₽`;
};
Плохо
// Плохо: нет явного возвращаемого типа.
export const formatPrice = (value: number) => {
return `${value} ₽`;
};
Работа с any/unknown
anyиспользовать только для временных заглушек.unknownсужать через проверки перед использованием.
Хорошо
const parse = (value: unknown): string => {
if (typeof value === 'string') {
return value;
}
return '';
};
Плохо
// Плохо: any отключает проверку типов.
const parse = (value: any) => value;
Настройка VS Code
Каждый проект содержит папку .vscode/ с конфигурацией редактора. Это гарантирует, что все участники команды работают с одинаковыми настройками форматирования, линтинга и расширениями.
Структура .vscode/
.vscode/
├── extensions.json # Рекомендуемые расширения
└── settings.json # Настройки редактора для проекта
Оба файла коммитятся в репозиторий.
Расширения
Файл .vscode/extensions.json определяет список расширений, которые VS Code предложит установить при открытии проекта.
// .vscode/extensions.json
{
"recommendations": [
"biomejs.biome",
"MyTemplateGenerator.mytemplategenerator",
"csstools.postcss"
]
}
| Расширение | Назначение |
|---|---|
| Biome | Линтинг и форматирование кода. Заменяет ESLint и Prettier |
| MyTemplateGenerator | Генерация файлов и папок из шаблонов .templates/ через контекстное меню |
| PostCSS Language Support | Подсветка синтаксиса и автодополнение для PostCSS (@custom-media, @nest и др.) |
Зачем это нужно
- Новый участник команды получает все нужные расширения одним кликом.
- Нет разночтений: все используют одинаковый форматтер и линтер.
- Расширения привязаны к проекту, а не к конкретному разработчику.
Настройки редактора
Файл .vscode/settings.json переопределяет пользовательские настройки VS Code на уровне проекта.
// .vscode/settings.json
{
"editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"quickfix.biome": "explicit",
"source.organizeImports.biome": "explicit"
},
"files.associations": {
"*.css": "postcss"
}
}
Разбор настроек
| Настройка | Значение | Что делает |
|---|---|---|
editor.defaultFormatter |
biomejs.biome |
Biome используется как единственный форматтер для всех файлов |
editor.formatOnSave |
true |
Код автоматически форматируется при каждом сохранении |
codeActionsOnSave.quickfix.biome |
explicit |
Biome автоматически применяет безопасные исправления при сохранении |
codeActionsOnSave.source.organizeImports.biome |
explicit |
Импорты сортируются и группируются автоматически при сохранении |
files.associations |
"*.css": "postcss" |
Все CSS-файлы открываются с подсветкой PostCSS вместо стандартного CSS |
Зачем это нужно
- Единый стиль кода -- форматирование происходит автоматически, невозможно закоммитить неформатированный код.
- Автофикс при сохранении -- распространённые ошибки линтинга исправляются без ручного вмешательства.
- Сортировка импортов -- импорты всегда в одном порядке, без конфликтов при мерже.
- PostCSS-подсветка -- кастомные at-правила (
@custom-media,@define-mixin) подсвечиваются корректно, а не как ошибки.
Что не должно быть в .vscode/
Не коммитятся файлы, специфичные для конкретного разработчика:
- Не коммитить: отладочные конфигурации с локальными путями, персональные сниппеты, настройки тем оформления.
- Коммитить: только
extensions.jsonиsettings.jsonс общими для команды настройками.
Структура проекта
Раздел описывает базовую структуру проекта Next.js (App Router) и принципы организации модулей на уровне папок и файлов.
Базовая структура проекта
src/
├── app/ # Слой app: роутинг, провайдеры, глобальные стили
│ ├── providers/ # Провайдеры и обёртки приложения
│ ├── styles/ # Глобальные стили, CSS-переменные, custom media
│ ├── layout.tsx # Корневой layout (провайдеры, стили, метаданные)
│ ├── page.tsx # Главная страница → HomeScreen
│ └── profile/
│ ├── page.tsx # → ProfileScreen
│ └── [id]/
│ └── page.tsx # → ProfileDetailScreen
├── screens/ # UI-компоненты страниц
│ ├── home/
│ │ ├── home.screen.tsx
│ │ └── index.ts
│ └── profile/
│ ├── profile.screen.tsx
│ └── index.ts
├── layouts/ # Общие шаблоны и каркасы страниц
│ └── main-layout/
│ ├── main-layout.layout.tsx
│ └── index.ts
├── widgets/ # Крупные блоки интерфейса
│ └── header/
│ ├── header.widget.tsx
│ └── index.ts
├── features/ # Пользовательские сценарии
│ └── auth-by-email/
│ ├── ui/
│ │ └── login-form.tsx
│ ├── model/
│ │ └── auth-by-email.store.ts
│ ├── auth-by-email.feature.tsx
│ └── index.ts
├── entities/ # Бизнес-сущности
│ └── user/
│ ├── model/
│ │ └── user.store.ts
│ ├── user.entity.tsx
│ └── index.ts
└── shared/ # Общие ресурсы проекта
├── ui/ # Повторно используемые UI-элементы
│ └── icon/
│ ├── styles/
│ │ └── icon.module.css
│ ├── types/
│ │ └── icon.interface.ts
│ ├── icon.tsx
│ └── index.ts
├── lib/ # Утилиты и хелперы
├── services/ # Общие сервисы и клиенты
├── config/ # Общие конфигурации и константы
└── assets/ # Ресурсы
├── images/
├── icons/
├── fonts/
└── video/
Слой app/
Папка app/ совмещает две роли: инициализация приложения (провайдеры, глобальные стили) и файловый роутинг Next.js (route-сегменты, layout.tsx, page.tsx).
providers/иstyles/-- это инфраструктура приложения, они не являются частью роутинга.- Route-сегменты (вложенные папки с
page.tsx) -- это роутинг Next.js. Они не содержат логики, только импортируют экраны изscreens/.
Компоненты, хуки, стили и утилиты не размещаются внутри route-сегментов -- всё это живёт в соответствующих слоях FSD.
Правила организации
- В слоях FSD (
features,entities,widgets,screensи т.д.)ui/используется только для дочерних элементов, которые относятся к модулю и не экспортируются отдельно. Главные компоненты, которые составляют сам слой, держат собственные*.feature.tsx,*.widget.tsxи т. п., аui/служит для вспомогательных мелких компонентов. - В
shared/ui/хранятся базовые UI-элементы/компоненты, которыми пользуются сразу несколько модулей; в этом случае они экспортируются наружу и не считаются «дочерними» для слоя. - Если модуль строится вокруг «главного» компонента (
*.feature.tsx,*.screen.tsx,*.widget.tsx), помещайте его в корень модуля и экспортируйте черезindex.ts. Проверяйте, чтоui/не используется просто как «контейнер» слоя. - Каждый слой и модуль хранится в собственной папке.
- Внутренние реализации разделяются на
ui/,model/,styles/,helpers/,lib/,api/. - Публичный API модуля объявляется в
index.ts. - Внутренние файлы не импортируются напрямую извне.
- Не смешивать ответственность разных слоёв в одном модуле.
Пример организации структуры
Плохо -- плоская структура без архитектуры:
src/
├── components/
│ ├── Header.tsx
│ ├── LoginForm.tsx
│ ├── UserCard.tsx
│ └── Button.tsx
├── hooks/
│ ├── useAuth.ts
│ └── useUser.ts
├── api/
│ ├── auth.ts
│ └── user.ts
├── styles/
│ ├── header.module.css
│ └── login.module.css
├── types/
│ └── user.ts
└── utils/
└── format.ts
Нет слоёв, нет границ ответственности -- Header и Button лежат рядом, хотя это разные уровни абстракции. LoginForm знает про API напрямую. При росте проекта components/ превращается в свалку.
Хорошо -- та же функциональность в FSD:
src/
├── widgets/
│ └── header/
│ ├── header.widget.tsx
│ └── index.ts
├── features/
│ └── auth-by-email/
│ ├── ui/
│ │ └── login-form.tsx
│ ├── model/
│ │ └── auth.store.ts
│ ├── auth-by-email.feature.tsx
│ └── index.ts
├── entities/
│ └── user/
│ ├── ui/
│ │ └── user-card.tsx
│ ├── model/
│ │ └── user.store.ts
│ ├── user.entity.tsx
│ └── index.ts
└── shared/
├── ui/
│ └── button/
│ ├── button.tsx
│ └── index.ts
└── lib/
└── format.ts
Каждый элемент на своём слое, с публичным API и чёткими границами ответственности.
Компоненты
Раздел описывает правила создания UI‑компонентов. Эти правила обязательны для всех слоёв FSD: app, screens, layouts, widgets, features, entities, shared.
Базовая структура компонента
Минимальный набор файлов: компонент, стили, типы и публичный экспорт.
container/
├── styles/
│ └── container.module.scss
├── types/
│ └── container.interface.ts
├── container.tsx
└── index.ts
Пример базового компонента
styles/container.module.scss
.root {}
В CSS Modules использование имени класса .root — это общепринятое соглашение (best practice)
types/container.interface.ts
import type { HTMLAttributes } from 'react'
/**
* Параметры контейнера.
*/
export interface ContainerProps extends HTMLAttributes<HTMLDivElement> {}
Интерфес параметров компонента всегда наследует свойства своего тега: div, button, итд..
container.tsx
import type { FC } from 'react'
import cl from 'clsx'
import type { ContainerProps } from './types/container.interface'
import styles from './styles/container.module.scss'
/**
* Контейнер с адаптивной максимальной шириной.
*
* Используется для:
* - ограничения ширины контента
* - центрирования содержимого
* - построения адаптивной сетки страницы
*/
export const Container: FC<ContainerProps> = ({ className, ...htmlAttr }) => {
return (
<div {...htmlAttr} className={cl(styles.root, className)}>
Container...
</div>
)
}
- Компонент объявляется через
constи экспортируется именованно. - Пропсы деструктурируются в сигнатуре; если их больше двух — деструктуризацию переносим в тело компонента.
- Из пропсов отдельно извлекаются
classNameи...htmlAttr, чтобы корректно объединять классы и прокидывать остальные атрибуты. cl— короткое имя функции для конкатенации CSS‑классов.FC<>используется для декларацииchildren.
index.ts
export { Container } from './container'
Шаблоны и генерация кода
Создание компонентов — только через шаблоны. Ручное создание файловой структуры компонента запрещено. Это обеспечивает единообразие каркаса, одинаковые папки и имена файлов, уменьшает ручные ошибки и ускоряет старт работы.
После генерации через @gromlab/create — проверить название компонента/файлов и заполнить описание назначения. Подробный порядок действий и перечень обязательных шаблонов — в разделе «Workflow».
Вложенные (дочерние) компоненты
Если для реализации функционала нужны компоненты, которые используются только внутри текущего компонента, создавайте их как вложенные в папке ui/. Такие компоненты не экспортируются наружу и используются только локально.
Вложенные компоненты подчиняются тем же правилам по структуре, именованию и стилю (включая папку styles/ для их стилей).
Page-level компоненты
Специальные файлы Next.js App Router, которые фреймворк использует по соглашению: layout.tsx, page.tsx, loading.tsx, error.tsx, not-found.tsx, template.tsx.
Общие правила
- Экспорт через
export default function— конвенция Next.js. - Типизация через
PropsWithChildrenили явный интерфейс. - Каждая страница (
page.tsx) должна содержатьmetadataсtitleиdescription. - Минимум логики — page-level компоненты делегируют работу экранам, виджетам и фичам.
- Стили в page-level компонентах не используются — стилизация внутри вызываемых компонентов.
layout.tsx
Корневой layout — точка подключения провайдеров, глобальных стилей и метаданных.
import type { PropsWithChildren } from 'react'
import type { Metadata } from 'next'
import { ColorSchemeScript, MantineProvider } from '@mantine/core'
import '@mantine/core/styles.css'
import './globals.css'
export const metadata: Metadata = {
title: {
default: 'App',
template: '%s | App',
},
description: 'Описание приложения',
metadataBase: new URL('https://example.com'),
openGraph: {
type: 'website',
locale: 'ru_RU',
siteName: 'App',
images: [
{
url: '/og-image.png',
width: 1200,
height: 630,
alt: 'App',
},
],
},
twitter: {
card: 'summary_large_image',
},
}
export default function RootLayout({ children }: PropsWithChildren) {
return (
<html lang="ru" suppressHydrationWarning>
<head>
<ColorSchemeScript />
</head>
<body>
<MantineProvider>
{children}
</MantineProvider>
</body>
</html>
)
}
Вложенный layout — для секции с общей обёрткой (sidebar, header):
import type { PropsWithChildren } from 'react'
import { DashboardLayout } from '@/shared/ui/dashboard-layout'
export default function Layout({ children }: PropsWithChildren) {
return (
<DashboardLayout>
{children}
</DashboardLayout>
)
}
page.tsx
Тонкий файл — только импорт и рендер экрана. Логика, стили и зависимости размещаются в экране, не в page.tsx.
import type { Metadata } from 'next'
import { HomeScreen } from '@/screens/home'
export const metadata: Metadata = {
title: 'Главная',
description: 'Главная страница приложения',
}
export default function HomePage() {
return <HomeScreen />
}
С параметрами маршрута:
import type { Metadata } from 'next'
import { ProfileScreen } from '@/screens/profile'
export const metadata: Metadata = {
title: 'Профиль',
description: 'Страница профиля пользователя',
}
interface ProfilePageProps {
params: Promise<{ id: string }>
}
export default async function ProfilePage({ params }: ProfilePageProps) {
const { id } = await params
return <ProfileScreen id={id} />
}
Каждая страница должна содержать metadata с title — он подставится в шаблон из корневого layout: Профиль | App.
loading.tsx
Состояние загрузки. Показывается пока загружается контент страницы.
export default function Loading() {
return <div>Загрузка...</div>
}
error.tsx
Обработка ошибок. Обязательно 'use client' — error boundary работает только на клиенте. Разметку выносим в экран.
'use client'
import type { FC } from 'react'
import { ErrorScreen } from '@/screens/error'
interface ErrorPageProps {
error: Error & { digest?: string }
reset: () => void
}
const ErrorPage: FC<ErrorPageProps> = ({ error, reset }) => {
return <ErrorScreen error={error} reset={reset} />
}
export default ErrorPage
not-found.tsx
Страница 404. Показывается когда маршрут не найден. Разметку выносим в экран.
import type { Metadata } from 'next'
import { NotFoundScreen } from '@/screens/not-found'
export const metadata: Metadata = {
title: 'Страница не найдена',
description: 'Запрашиваемая страница не существует',
}
export default function NotFound() {
return <NotFoundScreen />
}
template.tsx
Аналог layout, но пересоздаётся при каждой навигации (не сохраняет состояние). Используется редко — для анимаций переходов между страницами.
import type { PropsWithChildren } from 'react'
export default function Template({ children }: PropsWithChildren) {
return <div>{children}</div>
}
::: v-pre
Шаблоны и генерация кода
Как работают шаблоны, как их создавать, синтаксис переменных и как генерировать код с помощью расширения VS Code и CLI.
Структура шаблонов
Все шаблоны лежат в .templates/ в корне проекта. Каждая папка — отдельный шаблон.
.templates/
├── component/ # шаблон компонента
│ └── {{name.kebabCase}}/
│ ├── styles/
│ │ └── {{name.kebabCase}}.module.css
│ ├── types/
│ │ └── {{name.kebabCase}}.interface.ts
│ ├── {{name.kebabCase}}.tsx
│ └── index.ts
└── store/ # шаблон Zustand стора
└── {{name.kebabCase}}/
├── {{name.kebabCase}}.store.ts
├── {{name.kebabCase}}.type.ts
└── index.ts
Синтаксис шаблонов
Переменные работают в именах файлов/папок и внутри файлов. Базовая переменная — name.
{{variable}}
Модификаторы меняют регистр и формат записи:
{{name.pascalCase}} → MyButton
{{name.camelCase}} → myButton
{{name.kebabCase}} → my-button
{{name.snakeCase}} → my_button
{{name.screamingSnakeCase}} → MY_BUTTON
Как создать новый шаблон
- Создать папку в
.templates/с именем шаблона (напримерhook). - Внутри разместить файлы и папки, используя
{{name}}и модификаторы в именах и содержимом. - Шаблон сразу доступен и в расширении VS Code, и в CLI.
Пример — создание шаблона для хука:
.templates/
└── hook/
└── {{name.kebabCase}}/
├── {{name.kebabCase}}.hook.ts
└── index.ts
// .templates/hook/{{name.kebabCase}}.hook.ts
export const {{name.camelCase}} = () => {
}
// .templates/hook/index.ts
export { {{name.camelCase}} } from './{{name.kebabCase}}.hook'
Примеры шаблонов
Шаблон компонента
// .templates/component/index.ts
export { {{name.pascalCase}} } from './{{name.kebabCase}}'
// .templates/component/types/{{name.kebabCase}}.interface.ts
import type { HTMLAttributes } from 'react'
/**
* Параметры {{name.pascalCase}}.
*/
export interface {{name.pascalCase}}Props extends HTMLAttributes<HTMLDivElement> {}
// .templates/component/{{name.kebabCase}}.tsx
import type { FC } from 'react'
import cl from 'clsx'
import type { {{name.pascalCase}}Props } from './types/{{name.kebabCase}}.interface'
import styles from './styles/{{name.kebabCase}}.module.css'
/**
* {{name.pascalCase}}.
*/
export const {{name.pascalCase}}: FC<{{name.pascalCase}}Props> = ({ className, ...htmlAttr }) => {
return (
<div {...htmlAttr} className={cl(styles.root, className)}>
{{name.kebabCase}}
</div>
)
}
/* .templates/component/styles/{{name.kebabCase}}.module.css */
.root {
}
Генерация через VS Code
MyTemplateGenerator — расширение для генерации файлов и папок из шаблонов через интерфейс редактора.
- ПКМ на целевой папке в проводнике VS Code.
- Generate from template → выбрать шаблон.
- Ввести имя (например
button) — расширение подставит его во все переменные{{name}}.
Генерация через CLI
@gromlab/create — CLI для генерации из тех же шаблонов. Используется через npx, глобальная установка не требуется.
npx @gromlab/create <шаблон> <имя> <путь>
| Команда | Что создаёт |
|---|---|
npx @gromlab/create component button src/shared/ui |
Компонент |
npx @gromlab/create feature auth src/features |
Фичу |
npx @gromlab/create widget header src/widgets |
Виджет |
npx @gromlab/create entity user src/entities |
Сущность |
npx @gromlab/create layout admin src/layouts |
Layout |
npx @gromlab/create store auth src/shared/model |
Стор |
:::
Стили
Раздел описывает правила написания CSS: PostCSS Modules, вложенность, медиа-запросы, переменные, форматирование.
Общие правила
- Только PostCSS и CSS Modules для кастомной стилизации.
- Подход Mobile First — стили пишутся от мобильных к десктопу.
- Именование классов —
camelCase(.root,.buttonNext,.itemTitle). - Модификаторы — отдельный класс с
_, применяется через&._modifier.
Хорошо
.submitButton {
padding: 8px 16px;
&._disabled {
opacity: 0.5;
}
}
Плохо
/* Плохо: kebab-case и вложенный элемент вместо отдельного класса. */
.submit-button {
padding: 8px 16px;
&__icon {
margin-right: 8px;
}
}
Вложенность
- Вложенность селекторов запрещена.
- Исключения:
- Псевдоклассы:
&:hover,&:active,&:focus,&:disabledи т.д. - Псевдоэлементы:
&::before,&::after. - Медиа-запросы:
@media. - Модификаторы:
&._active,&._disabled.
- Псевдоклассы:
- Каждый вложенный блок отделяется пустой строкой от предыдущих свойств.
Хорошо
.card {
padding: 16px;
background-color: var(--color-bg);
&:hover {
background-color: var(--color-bg-hover);
}
&::after {
content: '';
display: block;
}
&._highlighted {
border-color: var(--color-primary);
}
@media (--md) {
padding: 24px;
}
}
.cardTitle {
font-size: 16px;
@media (--md) {
font-size: 20px;
}
}
Плохо
/* Плохо: вложенность селекторов, нет пустых строк между блоками. */
.card {
padding: 16px;
.cardTitle {
font-size: 16px;
}
&:hover {
background-color: var(--color-bg-hover);
}
}
Медиа-запросы
- Только Custom Media Queries:
@media (--md) {}. - Запрещены произвольные breakpoints:
@media (min-width: 768px). @mediaпишется только внутри селектора.- Запрещено писать
@mediaна верхнем уровне с селекторами внутри.
Хорошо
.sidebar {
display: none;
@media (--md) {
display: block;
}
}
.sidebarTitle {
font-size: 14px;
@media (--md) {
font-size: 18px;
}
}
Плохо
/* Плохо: @media на верхнем уровне с селекторами внутри. */
@media (--md) {
.sidebar {
display: block;
}
.sidebarTitle {
font-size: 18px;
}
}
/* Плохо: произвольный breakpoint вместо custom media. */
.sidebar {
@media (min-width: 992px) {
display: block;
}
}
CSS-переменные
- Цвета (
--color-*), отступы (--space-*), скругления (--radius-*) определяются вapp/styles/variables.cssчерез:root. - Файл переменных подключается один раз в корневом layout/entry point — после этого переменные доступны глобально через каскад.
- Не дублировать магические значения в компонентах.
Хорошо
/* app/styles/variables.css */
:root {
--color-primary: #3b82f6;
--color-bg: #ffffff;
--color-bg-hover: #f5f5f5;
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--radius-1: 4px;
--radius-2: 8px;
}
/* компонент */
.card {
padding: var(--space-3);
border-radius: var(--radius-2);
background-color: var(--color-bg);
}
Плохо
/* Плохо: магические значения вместо переменных. */
.card {
padding: 12px;
border-radius: 8px;
background-color: #ffffff;
}
Custom Media
- Breakpoints определяются через Custom Media Queries в
app/styles/media.css. - Custom media подключаются глобально через конфиг PostCSS (плагин
postcss-custom-media) — не импортировать в файлы стилей.
/* app/styles/media.css */
@custom-media --sm (min-width: 36em);
@custom-media --md (min-width: 62em);
@custom-media --lg (min-width: 82em);
Импорт стилей
- Стили компонента импортируются только внутри своего компонента.
- Запрещено импортировать стили одного компонента в другой.
- Custom media не импортируются в файлы стилей — они подключаются глобально через конфиг PostCSS.
Форматирование
- Пустая строка между селекторами верхнего уровня.
- Пустая строка перед каждым вложенным блоком (медиа, псевдокласс, модификатор).
Хорошо
.userBar {
display: none;
color: var(--color-text);
@media (--md) {
display: flex;
}
}
.userBarButton {
background-color: var(--color-bg);
&:hover {
background-color: var(--color-bg-hover);
}
&._active {
background-color: var(--color-primary);
}
}
Плохо
/* Плохо: нет пустых строк между селекторами и вложенными блоками. */
.userBar {
display: none;
color: var(--color-text);
@media (--md) {
display: flex;
}
}
.userBarButton {
background-color: var(--color-bg);
&:hover {
background-color: var(--color-bg-hover);
}
&._active {
background-color: var(--color-primary);
}
}
Единицы измерения
px— основная единица измерения.- Остальные (
em,rem,%,vh/vw) — допускаются по необходимости дизайна.
Порядок CSS-свойств
В стилях рекомендуется придерживаться логического порядка свойств:
- Позиционирование (
position,top,left,z-index). - Блочная модель (
display,width,height,margin,padding). - Оформление (
background,border,box-shadow,border-radius). - Текст (
font,color,text-align,line-height). - Прочее (
transition,animation,opacity,cursor).
Комментарии
- Желательно не писать комментарии в CSS.
- Исключение — нетривиальные хаки и обходные решения, к которым стоит оставить пояснение.