Використання Feature-Sliced Design(FSD) у Next.js
Вступ
Всім привіт! 👋 Я Frontend-розробник початківець. І мабуть багато хто з колег, особливо початківців, не раз стикався з проблемою: як організувати архітектуру проєкту? Що куди складати? Яку архітектуру взагалі використати?
Насправді чіткої відповіді немає. Немає й універсальної інструкції, бо все залежить від самого проєкту та обраного стеку. Але у своєму останньому pet-проєкті я захотів розібратися і попрацювати з популярним архітектурним підходом Feature-Sliced Design (FSD). Причому використати його разом із новим для мене фреймворком Next.js.
Прошу не судити строго — я сам початківець і завжди відкритий до зауважень та порад. Можливо, комусь цей досвід стане у пригоді і хтось знайде для себе щось цікаве та корисне. Мені здається, що вийшло доволі логічно та зручно, особливо якщо у майбутньому розширювати функціонал.
Коротко про DevQuizzer
Як приклад хочу розповісти про свій останній pet-проєкт — DevQuizzer, застосунок з квізами для розробників.
Функціонал:
- форма для налаштування квізу (кількість питань, складність, категорії);
- проходження квізу (одна/декілька відповідей, перехід між питаннями, повернення назад, відстеження прогресу);
- сторінка результатів (можна переглянути відповіді з поясненнями, завантажити листівку з результатами та поділитися);
- перемикач теми (світла/темна/автоматична).
Під час роботи найбільше користувався оригінальною документацією: overview. Є там і гайд для Next.js, але відверто — частина того, що описано, мені не знадобилася, а деяких речей якраз не вистачало. Тому я зробив трохи видозмінений варіант архітектури.
Повна структура застосунку
Щоб краще показати повну картину, ось структура застосунку:
app/ # Next.js app directory (home)/components # Головна сторінка та її блоки compose-quiz/ # Конфігуратор квізу quiz/ # Сторінка проходження results/ # Результати layout.tsx # Загальний Layout globals.css # Глобальні стилі features/ # Бізнес-функціонал params/ # Конфігурація квізу ├── api/ # Запити на сервер для отримання категорій ├── store/ # Slice зі станом параметрів квізу └── ui/ # Компоненти форми для вибору налаштувань quiz/ # Логіка квізу та результатів ├── api/ # Запит на сервер для нового квізу ├── hooks/ # Кастомні хуки (localStorage, resetQuiz, навігація) ├── store/ # Slice для квізу та відповідей користувача └── ui/ # Усі UI-компоненти для проходження та результатів themeSwitcher/ # Перемикач теми ├── store/ # Slice для збереження теми └── ui/ # Компоненти перемикача shared/ # Повторно використовувані частини providers/ # Провайдери (Tanstack Query, Material UI) schemas/ # Zod-схеми та типи ui/ # Header, Footer, Menu theme.ts # Тема для Material UI utils/ # Допоміжні функції (4 невеликі утиліти)
Чому саме FSD?
Мене привабило кілька моментів:
- Масштабованість. З появою нових фіч легко зрозуміти, куди їх додавати.
- Ізоляція. Кожна фіча має свій store, API, UI і не «лізе» у чужий простір.
- Прозорість. Компоненти логічно згруповані, немає хаосу.
- Гнучкість. Це не догма, а підхід, який можна підлаштувати.

1. Шари (Layers)
У моєму проєкті вийшло так:
- app/ # Next.js app directory (routing, pages, layout) - features/ # Feature modules (quiz, params, themeSwitcher) - shared/ # Shared UI, providers, schemas, types - utils/ # Utility functions
Цього загалом було достатньо для такого невеликого застосунку, як мій. Одразу хочеться розібрати шари, які я не використав, або замінив:
- pages/ — не потрібен у Next.js (усе в
app/). - widgets/ — віджетів у мене просто немає.
- entities/ — для мого випадку зайвий шар. Квіз як сутність добре лягає у features.
- utils/ — я додав сам, бо звик, що різні утиліти лежать в окремому місці. Загалом їх можна винести у кожну конкретну фічу, але у мене застосунок невеликий, тож всі мої (4) утиліти можуть зберігатися в одному місці, і заплутатися буде важко.
2. Слайси (Slices)
Тепер можна перейти до більш детального розбору кожного шару окремо, а саме розібрати Slices:
1.Шар app
З app все доволі просто, бо тут, як я писав раніше, я зберігаю сторінки, тому структура виглядає наступним чином:
app ├── (home)/ # Головна сторінка ├── compose-quiz/ # Конфігуратор квізу ├── quiz/ # Сторінка проходження ├── results/ # Результати ├── layout.tsx # Layout із провайдерами, header, footer └── globals.css # Глобальні стилі
В цілому все просто і лаконічно, особливо немає на чому зупинятися, тому можна більш детально розібрати структуру всередині сторінок. Кожна сторінка має наступну структуру:
page ├── page.tsx ├── page.module.scss
Всередині page.tsx у мене описана структура кожної сторінки. Наприклад:
imports...
export default function ComposeQuiz() {
return (
<Container className={styles.page} maxWidth="lg">
<Typography variant="h2" component="h1">
✏️ Create your personal quiz
</Typography>
<ParamsForm />
</Container>
);
}
👉 Але (home) page має ще додаткову папку components. Мені здалося зручним: головна сторінка має більший розмір за інші, і я вирішив розбити її на окремі UI-блоки. Тому з’явилася така структура:
components ├── AboutAPI.tsx ├── APIFeature.tsx ├── HeroSection.tsx ├── HowItWorks.tsx ├── Step.tsx ├── TechStack.tsx └── Styles.module.scss # Загальні стилі для блоків
2.Шар features
Тут усе виглядає доволі просто, але не настільки очевидно, як у app/.
features ├── params # Конфігурація квізу ├── quiz # Основний функціонал └── themeSwitcher # Перемикач теми
Оскільки структура слайсів тут трохи складніша ніж у попередньому шарі, то думаю краще зупинитися на цьому детальніше у наступному розділі. Тут можу додати, що початково планував поділити quiz та results на різні слайси, але виникли труднощі: обидві фічі мали спільний store. Імпортувати його з одного в інший виглядало заплутано, а ще це порушувало принципи FSD та SOLID (Single Responsibility, ізольованість). Тому я вирішив залишити їх в одному slice. Чи це правильне рішення — не впевнений, буду радий вашим порадам у коментарях.
3.Шар shared
Також доволі цікавий шар, бо кожен буде зберігати там щось своє, залежно від застосунку та технологій. У мене вийшло так:
shared ├── providers # провайдери для Tanstack Query і Material UI ├── schemas # Zod-схеми та типи ├── ui # Header, Footer, Menu └── theme.ts # Тема для Material UI
Я відмовився від окремої папки types, бо всі основні типи виводжу із Zod-схем. А якщо треба додати
3. Сегменти (Segments)
Останній концепт, що варто розібрати у FSD, — це сегменти. Тобто ми ділимо кожен slice, або краще сказати кожну фічу (мені так більше подобається 😏). Найкращим прикладом для дослідження буде шар features, який ми у попередньому розділі залишили на потім. Тож маємо всього 3 фічі, і структура у них наступна:
features ├── params │ ├── api │ ├── store │ └── ui ├── quiz │ ├── api │ ├── hooks │ ├── store │ └── ui └── themeSwitcher ├── store └── ui
Як можна помітити, структура трохи відрізняється у різних фічах, але це більше залежить від складності та функцій окремої фічі.
- params Тут ми маємо наступний функціонал:
- форма для запису параметрів квізу;
- запит на сервер можливих категорій квізу;
- маленький store, де ми зберігаємо обрані параметри.
- quiz Ця фіча найскладніша у застосунку, і тут структура трохи важча, але некритично. Вона виконує наступні функції:
- робить запит на сервер для нового квізу;
- записує та перевіряє відповіді користувача;
- виводить результати;
- дозволяє навігацію між питаннями;
- працює з localStorage;
- забезпечує можливість перезапуску вікторини.
- themeSwitcher Тут функціонал простіший:
- зберігає обрану користувачем тему у store;
- надає UI-компонент для перемикання теми.
Найкраще розглядати структуру фічі на прикладі найскладнішої з них — quiz:
- api — описання всіх запитів до сервера;
- hooks — тут описані кастомні хуки для роботи цієї фічі (це можуть бути допоміжні хуки по роботі з localStorage, легші хуки на кшталт resetQuiz, якщо треба обʼєднати декілька дій, ну і інше, думаю можна на цьому не зупинятися);
- store — для кожної фічі правильніше використовувати окремий store, тут у store я зберігаю нормалізовано сам квіз та відповіді користувача;
- ui — тут зберігаються всі UI-компоненти відповідної фічі окремо по папках, як зазвичай.
Можу додати по ui, що раніше я організовував компоненти з глибоким вкладенням, тобто:
ParentComponent/ ├── ParentComponent.tsx ├── ParentComponent.module.scss ├── Child1/ │ ├── Child1.tsx │ └── Child1.module.scss └── Child2/ └── ...
Але в даному випадку вирішив відмовитися від вкладень і просто всі компоненти зберігаю поруч в одній папці, навіть child, виглядає так:
ParentComponent/ ├── ParentComponent.tsx ├── ParentComponent.module.scss ├── Child1.tsx ├── Child2.tsx
Кожний обере варіант для себе, особисто мені більше подобається перший, виглядає більш структуровано, але і коли вкладених компонентів стає більше, то вже важче орієнтуватися по такій структурі, бо глибина може бути понад 5 папок.
Висновок
Сподіваюся, ця стаття стане комусь корисною. Це мій особистий досвід і моє бачення можливого застосування FSD разом із Next.js. Це моя перша обʼємна публікація, тому буду радий будь-яким порадам.
Я чудово розумію, що у кожного розробника може бути свій підхід, свої уподобання та навіть власні «лайфхаки» при організації архітектури. У когось проєкти більші, у когось менші, і те, що добре спрацювало у мене, може не підійти в іншому випадку. Але головне — мати логіку та послідовність, щоб проєкт розвивався без хаосу.
Цей досвід показав мені, що навіть невеликий pet-проєкт може стати чудовим майданчиком для практики серйозних архітектурних підходів. І якщо ви тільки починаєте знайомство з FSD, то раджу спробувати застосувати його хоча б у маленькому проєкті — це допомагає зрозуміти сильні та слабкі сторони підходу.
Переваги мого підходу:
- структура виглядає логічно та зрозуміло;
- легко масштабувати й розширювати;
- менше шансів отримати «спагеті-код».
А як ви організовуєте архітектуру у своїх проєктах? Поділіться в коментарях. 🙌
6 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарівКрута стаття, проте є декілька не зрозумілих моментів
Чому б не розмістити utils в shared/lib, здається що їм там було б саме місце, з рахунок чого стратегія шарів б не порушувалося
Щодо entities, хочу підсвітити те, що features не може містити в собі декларацію сутності by-design. В твоєму випадку мені здається, що варто було б рухатися саме з сутністю та фічами під неї. Про імпорти з features в shared вже відписали
Проте загалом — гідна реалізація та цікава стаття
Дякую за оцінку та корисні поради, обовʼязково візьму до увагу та спробую виправити всі протиріччя
Утиліти лежать у src, але фактично мають доступ і до shared, і до features. Це схоже на використання any у TypeScript: формально шари створені, проте існує директорія, яка виходить за межі ієрархії. Додатково, у shared трапляються імпорти з features, що знову порушує принципи імпортів з шарів. Отже: з cohesion ми впоралися, а з coupling — програли.
Додатково варто б подивитися на інтерфейси слайсів, тому що глибокі імпорти руйнують ту магію, за якої ми знаємо публічний інтерфейс і можемо редагувати що завгодно всередині.
Можна поставити eslint правила, щоб налаштувати все вищезазначене. P.S. в цілому, приємна архітектура.
FSD наче і нічого, але ж розробили цю архітектуру москалі, нам в Україні треба свій варіант feature-based підходу.
Доречі я нічого поганого не бачу в глибокій вкладеності, для пошуку компонента просто тицни по ньому в React devtools, а потім шукай по назві в коді. Чим вкладеність так лякає?
Мені загалом також більше імпонує варіант із вкладеністю. Дійсно, так виглядає більш структуровано. Але зустрічав і протилежні думки, що такий підхід тільки більше заплутує(тому і вирішив спробувати попрацювати без вкладеності, щоб зрозуміти, що мені більше до вподоби).
Стосовно москалів, тоді можливо краще розцінювати це просто як мою інтерпретацію feature-based без привʼязки саме до них))
Тримати під контролем владенність краще через те, що:
1) Добре, коли в проєкті легко орієнтуватися через IDE, розуміючи, з чого складається фіча, які компоненти охоплює і т.п. Девтулзи — ок, але не завжди захочеться в них лізти;
2) Зростає ментальна складність. В одному слайсі починаються імпорти по типу «.../.../../../utils/...», розробник не розуміє всієї картини відразу.