Feature-Sliced Design(FSD)は、フロントエンドのソースコードを「機能」と「ビジネス上の概念」で分けて配置する設計方法論です。components・hooks・utilsのように技術の種類でフォルダを切ると、ログイン機能ひとつの部品が複数のフォルダに散らばり、改修や削除のたびに影響範囲を追い直すことになります。FSDはLayers・Slices・Segmentsの3階層と、import方向の決まりでコードの配置候補と依存関係を制限します。
この記事では、2024年11月に公開された現行仕様v2.1に沿って3階層とimportルールを整理し、公式リンターSteiger 0.8.0を違反入りの構成で実行した結果、既存プロジェクトへの導入手順、採用を見送る条件までを扱います。
まとめ:Feature-Sliced Designの要点
- FSDは、Layers(役割)→Slices(ドメイン)→Segments(目的)の3階層でコードの置き場所を決める。レイヤーは app・pages・widgets・features・entities・shared の6層で、processes層は非推奨。widgets層も公式は使用を勧めていない。
- importは自分より厳密に下のレイヤーからだけ行う。同じレイヤーの別スライスは原則参照せず、entities間では
@x記法を使える。同一スライス内と、スライスを持たないapp・shared内の参照は可能。 - v2.1の中心はpages first。まずpagesに書き、複数ページで再利用されたものだけをfeatures・entitiesへ切り出す。
- ルールの検査は公式リンターSteigerで自動化できる。
npx steiger ./srcで、上位レイヤーからのimportやPublic APIの迂回をファイルと行番号つきで指摘する。 - 画面が数枚の小規模アプリや短期間で捨てるプロトタイプには過剰。機能が増え続け、複数人で長く保守するアプリで効果が出る。
Feature-Sliced Designとは:機能単位で分けるフロントエンド設計
FSDは、Reactをはじめとするコンポーネント指向のフロントエンドで、コードを機能(feature)とビジネス上の概念(entity)を軸に分割するための設計標準です。仕様は公式ドキュメント(Feature-Sliced Design Overview)が定義し、GitHubのfeature-sliced/documentationで2024年11月13日に公開されたv2.1が2026年10月時点の現行版です。
従来の種類別フォルダ(folder-by-type)では、ログイン機能のUIはcomponents/、状態はstore/、通信はapi/、型はtypes/へと分かれます。FSDはこれを反転させ、まず機能でまとめ、その内部を目的で分ける構造を、明文化されたルールとして与えます。サーバーサイドで機能ごとに縦に切るVertical Slice Architecture(バーティカルスライス)とは?機能単位の設計とC#・Go実装例と発想は近いものの、FSDはUI・状態・ルーティングの依存というフロントエンド特有の問題に絞っています。
FSDの構造:Layers・Slices・Segmentsの3階層
FSDの構造は3つの入れ子で決まります。Layersの中にSlicesがあり、その中にSegmentsが入ります。
Layers:役割で分かれた6つのレイヤーと参照方向
Layersはアプリを役割で分けた最上位のフォルダです。公式のLayersリファレンスは上から次の順に定義しています。
| レイヤー | 置くもの | スライス |
|---|---|---|
| app | ルーティング・プロバイダ・グローバルスタイル | 持たない |
| processes | 複数ページにまたがる処理(非推奨) | 持つ |
| pages | ルートに対応する画面 | 持つ |
| widgets | 大きなUIブロック(公式は使用を推奨しない) | 持つ |
| features | 複数ページで再利用する操作(例:カート追加) | 持つ |
| entities | ビジネス上の概念(例:user、product) | 持つ |
| shared | UIキット・API接続・環境設定など | 持たない |
processes層は公式に「This layer has been deprecated」と明記され、中身をfeaturesとappへ移すよう求められています。新規設計で使う理由は無く、後述のSteigerもfsd/no-processesルールでエラーにします。
最重要のルールは参照方向です。スライス内のファイルは、自分より厳密に下のレイヤーにあるスライスだけをimportできます。featuresはentitiesやsharedを使えますが、entitiesはfeaturesを参照できません。依存が上から下の一方向に固定され、下ほど汎用・上ほど具体という並びが保たれます。appとsharedはスライスを持たない特別なレイヤーで、それぞれの内部のセグメント同士は参照し合えます。内側ほど抽象を保つクリーンアーキテクチャの依存性ルールと同じ発想を、フロントエンドのフォルダ構造に落としたものと捉えると理解しやすくなります。
Slices:ビジネスの語彙で命名するドメイン単位の区画
Slicesは、app・shared以外のレイヤーの中をビジネスのドメインで縦に割った区画です。features/add-to-cart、entities/userのように、技術名ではなくプロダクトの語彙で命名します。
スライスの独立性を守るため、同じレイヤーのスライス同士は直接importしません。features/add-to-cartの中からfeatures/auth-by-emailを読み込むのは違反で、両方を使いたいなら上のpagesで組み合わせます。この制約が「機能Aを消したら無関係のBが壊れた」という連鎖を防ぎます。
Segments:ui・api・model・lib・configの目的別分類
Segmentsはスライスの内部を目的で分けたフォルダです。標準の名前は次の5つで、必要なものだけ作ります。
| セグメント | 入れるもの |
|---|---|
| ui | コンポーネント・スタイル・表示用の整形 |
| api | バックエンドとの通信・リクエスト関数 |
| model | 型・スキーマ・ストア・ビジネスロジック |
| lib | そのスライスの中で使う補助コード |
| config | 設定値・フィーチャーフラグ |
独自の名前も付けられますが、中身の種類ではなく目的を表す名前にします。components・hooks・typesのような種類名のセグメントは、Steigerのfsd/segments-by-purposeルールが「This segment’s name should describe the purpose of its contents」として指摘します。
v2.1のpages first:ページ内配置と再利用時の切り出し
v2.1で変わったのは、画面を分解する順番です。公式のv2.0からの移行ガイドは「The main change in v2.1 is the new mental model for decomposing an interface — pages first」と書いています。v2.0では最初にentityやfeatureを洗い出して部品を組み立てる進め方が一般的でしたが、v2.1ではまずコードをpagesのスライスに置き、複数のページで必要になった時点で下のレイヤーへ移します。v2.0の構成はそのままv2.1でも有効で、破壊的変更はありません。
公式のLayersリファレンスは、pagesについて「チームが迷わず読める限り、1ページのスライスに置くコード量に上限は無い」とし、再利用しないUIブロックはページの中に置いたままでよいとしています。
featuresとentitiesを分ける判断基準
公式の記述から、どのレイヤーへ移すかは次の順で判断します。
| 問い | はいなら |
|---|---|
| 1つのページでしか使わないか | pagesのスライスに置いたままにする |
| ユーザーの操作で、複数ページで再利用するか | features(例:add-to-cart) |
| ビジネスが製品を説明するときの名詞か | entities(例:user、order) |
| 業務の文脈を持たない汎用UIか | shared(例:ボタン、モーダル) |
| 全ページ共通のレイアウトか | app |
公式はfeaturesについて「not everything needs to be a feature」と強調し、目安として「複数のページで再利用されていること」を挙げています。いいねボタンやカート追加のように動詞で言える操作はfeatures、userやproductのように名詞で言える概念とその型・取得関数はentitiesです。
widgets層は定義として残っていますが、公式Overviewは「using the Widgets layer is generally discouraged」と書いています。UIブロックはデータ取得や状態管理を抱えやすく、ユーザーの操作を担うfeaturesと責務が重なるためです。画面固有の組み合わせはpages、複数ページで使う操作とそのUIはfeatures、業務の文脈を持たないUIはshared、全体のレイアウトはappに置きます。
Public APIとimportルール:スライスの境界を守る仕組み
index.tsによる公開範囲の宣言
各スライス(app・sharedではセグメントごと)は、公開してよいものだけを再エクスポートするindex.tsを持ちます。外部のコードはこのindex.tsを経由してだけスライスを参照します。
// features/add-to-cart/index.ts(Public API)
export { AddToCartButton } from "./ui/AddToCartButton";
export { useCart } from "./model/useCart";
// pages/cart/ui/CartPage.tsx
import { AddToCartButton } from "features/add-to-cart"; // OK
import { cartStore } from "features/add-to-cart/model/store"; // NG:Public APIの迂回
公式のPublic APIリファレンスは目的を3つ挙げています。スライスの内部構造の変更から他の部分を守ること、挙動の大きな変更があればPublic APIの変更として表に出すこと、必要な部分だけを公開することです。2行目のような迂回はSteigerのfsd/no-public-api-sidestepが検出します。
@x記法:entities層だけに許された交差参照
entitiesは現実の概念を表すため、「userがお気に入りのproductを持つ」のように互いの型を参照したくなる場面があります。v2.1はこれを@x記法として標準化しました。entities/product/@x/user.tsは「productがuser専用に公開するAPI」で、公式は「A crossed with B」と読むと説明しています。
// entities/product/@x/user.ts(product が user にだけ見せる公開面)
export type { Product } from "../model/types";
// entities/user/model/types.ts
import type { Product } from "entities/product/@x/user";
export interface User { id: string; favorites: Product[] }
この構成でSteiger 0.8.0を実行すると、同じレイヤー間のimportでもfsd/forbidden-importsのエラーは出ませんでした。一方、featuresの間で同じことをすると「Forbidden cross-import」になります。公式も交差参照は最小限にし、entities層だけで使うよう求めています。
Public APIには落とし穴が3つあります。1つ目はexport * from "./ui"のようなワイルドカード再エクスポートで、公式は「スライスのインターフェースが分かりにくくなる」として避けるよう書いています。内部のモジュールまで公開されるので、後のリファクタリングが難しくなります。
2つ目はshared/uiとshared/libのindex.tsです。互いに無関係な部品の寄せ集めなので、1つのindex.tsで全部を再エクスポートすると、バンドラーによってはツリーシェイキングが効かず使わない部品までバンドルに入ります。公式はコンポーネントやライブラリごとに個別のindex.tsを置き、そこから直接importする形を勧めています。3つ目は循環importです。同じスライス内では自身のindex.tsを経由せず、対象ファイルへの相対パスでimportすると、公開用index.tsを介した循環を避けられます。ただし、内部ファイル同士の循環は別途確認が必要です。
ReactでのFSDディレクトリ構成の例
ECサイトを例に、Viteなどで作るReactアプリのsrc配下をFSDで組むと次のようになります。
src/
├── app/ # ルーター・プロバイダ・全ページ共通のレイアウト
├── pages/
│ ├── cart/
│ │ ├── ui/CartPage.tsx
│ │ └── index.ts
│ └── product-detail/
│ ├── ui/ProductDetailPage.tsx
│ ├── ui/ReviewList.tsx # このページでしか使わないのでページ内に置く
│ └── index.ts
├── features/
│ └── add-to-cart/ # 一覧・詳細・お気に入りの3ページで使う操作
│ ├── ui/
│ ├── model/
│ └── index.ts
├── entities/
│ ├── product/
│ │ ├── api/
│ │ ├── model/
│ │ ├── @x/user.ts
│ │ └── index.ts
│ └── user/
└── shared/
├── ui/button/ # 部品ごとに index.ts を置く
├── api/ # fetch のラッパー・ベースURL
└── config/
レビュー一覧のように1ページでしか使わない部品はpagesに残し、カート追加のように複数ページで使う操作だけをfeaturesに出すのがv2.1の形です。共通部品の粒度を決めるときは、アトミックデザインとは?5階層の分類基準とReactコンポーネント設計・3階層縮退の分類をshared/uiの中だけで使う組み合わせ方もあります。
Next.jsとの併用:app・pagesの改名とリンター設定
Next.jsはApp Routerがapp、Pages Routerがpagesをルーティング用のフォルダ名として予約しているため、FSDのレイヤー名と衝突します。公式のNext.jsガイドは、どちらのルーターを使う場合もFSDのappとpagesの両方を_app・_pagesに改名するよう求めています。ガイドはこの方法をSteigerと互換としていますが、Steiger 0.8.0で_app・_pagesの構成を検査すると、fsd/typo-in-layer-nameがLayer "_pages" potentially contains a typoとして2件のエラーを出しました。このルールだけを設定ファイルでoffにすると、_pagesはpagesレイヤーとして扱われ、entitiesから_pagesをimportすると「Forbidden import from higher layer」で止まります。Next.jsで使うときは、typo-in-layer-nameを外す設定を最初から入れておきます。
├── app/ # Next.js のルーティング
│ └── example/page.tsx # src/_pages/example を再エクスポートするだけ
└── src/
├── _app/ # FSD の app レイヤー
├── _pages/
│ └── example/
│ ├── ui/example.tsx
│ └── index.ts
├── features/
├── entities/
└── shared/
// app/example/page.tsx
export { ExamplePage as default, metadata } from "@/_pages/example";
Next.js側のフォルダはプロジェクト直下に置き、srcをFSDのコード専用にすると見通しがよくなります。App Routerの基本はNext.jsとは?16系App Routerで最小アプリを動かす手順と採用判断で扱っています。
SteigerによるFSDのルール違反検査
レイヤーの上下やPublic APIの迂回は、レビューで目視しても見落とします。SteigerはFSDの公式チームが開発するファイル構造用のリンターで、ESLintのプラグインではなく単独のCLIとして動きます。2026年10月20日に公開された0.8.0が最新で、READMEには「The project is in beta」と書かれています。動作にはNode.js 22.18.0以上が必要です。
Steigerの導入と実行コマンド
本体とFSD用のルール集@feature-sliced/steiger-pluginを開発依存として入れ、srcを指定して実行します。設定ファイルが無ければ推奨ルールがそのまま使われます。
npm i -D --save-exact [email protected] @feature-sliced/[email protected]
npx steiger ./src # 検査(違反があれば終了コード1)
npx steiger ./src --watch # ファイル変更のたびに再検査
npx steiger ./src --fix # 自動修正できる違反を直す
終了コードが1になるので、CIのジョブにそのまま入れれば違反を含むプルリクエストを止められます。--reporter jsonでJSON出力にもできます。
違反を仕込んだ構成での実行結果
Steiger 0.8.0(Node.js 26.5.0)で、上位レイヤーからのimport・同レイヤー間のimport・Public APIの迂回・種類名のセグメント・processes層を意図的に入れた構成を検査したところ、次の9件が出ました。
| 仕込んだ違反 | ルール | 件数 |
|---|---|---|
| entitiesからfeaturesをimport | fsd/forbidden-imports | 1 |
| features同士のimport | fsd/forbidden-imports | 1 |
| 1ページからしか使わないスライス | fsd/insignificant-slice | 3 |
| model内部ファイルを直接import | fsd/no-public-api-sidestep | 1 |
| index.tsの無いセグメント | fsd/public-api | 1 |
| componentsという名前のセグメント | fsd/segments-by-purpose | 1 |
| processes層 | fsd/no-processes | 1 |
出力はファイルと行・列の位置、メッセージ、ルールの説明ページへのリンクの組です。なおfeatures/add-to-cartのようなパスでimportする構成では、tsconfig.jsonのbaseUrl(またはpaths)が無いとSteigerがimport先を解決できず、fsd/forbidden-importsが1件も出ませんでした。エイリアスを使うプロジェクトでは、tsconfig.jsonが検査対象から見える位置にあることを先に確かめます。
┌ src/entities/product/model/store.ts:1:34
✘ Forbidden import from higher layer "features".
│
└ fsd/forbidden-imports: https://github.com/feature-sliced/steiger/tree/master/packages/steiger-plugin-fsd/src/forbidden-imports
┌ src/entities/product
✘ This slice has only one reference in slice "pages/cart". Consider merging them.
│
└ fsd/insignificant-slice: https://github.com/feature-sliced/steiger/tree/master/packages/steiger-plugin-fsd/src/insignificant-slice
見落とせないのはfsd/insignificant-sliceです。上位レイヤーからのimportなどを直した後も、画面が2枚の小さな構成ではentities・featuresの4スライスすべてが「1ページからしか参照されていないので統合を検討せよ」と指摘されました。v2.1のpages firstをリンターが強制している形で、v2.0流に最初から細かく切った既存プロジェクトにSteigerを入れると、この指摘が大量に出ます。
steiger.config.mjsによる検査ルールの調整
ルール集には21個のルールが定義され、推奨設定で有効になるのは17個です。fsd/no-wildcard-exports・fsd/import-localityなどの4個は既定で無効なので、使うなら設定ファイルで有効にします。逆に、導入初期にinsignificant-sliceを止めておくなら次のように書きます。
// steiger.config.mjs
import fsd from "@feature-sliced/steiger-plugin";
import { defineConfig } from "steiger";
export default defineConfig([
...fsd.configs.recommended,
{ rules: { "fsd/insignificant-slice": "off" } },
]);
READMEの例はファイル名がsteiger.config.jsですが、package.jsonに"type": "commonjs"があるプロジェクトでこの名前にすると「Cannot use import statement outside a module」で読み込みに失敗しました。npm 11のnpm init -yはこの行を既定で書き込みます。typeの行が無い場合はNode.js 26.5.0が警告を出したうえでESMとして読み直し、検査は通りました。拡張子を.mjsにすればどちらの場合も警告なしで読み込まれ、上の設定で残りの指摘は0件になりました。
Next.jsで_app・_pagesを使う構成では、同じ配列に{ rules: { "fsd/typo-in-layer-name": "off" } }を足します。
既存プロジェクトにFSDを段階的に導入する手順
既存のコードを一度に移す必要はありません。公式のOverviewは「Incremental adoption」として次の3段階を示しています。
- appとsharedを整える:ルーティング・プロバイダ・グローバルスタイルを
appへ、ドメインに依存しない共通部品と通信処理をsharedへ、モジュール単位で少しずつ移します。 - 既存のUIをpagesとwidgetsへ振り分ける:画面単位でpagesへ移します。この段階ではimportがFSDのルールに違反していても構いません。
- 違反を解消しながらentities・featuresを切り出す:複数ページで重複している型や操作を見つけた順に下のレイヤーへ移し、importの違反を減らしていきます。
Steigerは2段階目から入れると、残っている違反の件数が移行の進み具合の指標になります。公式は、リファクタリングと同時に大きなentityを新しく足すことは避けるよう書いています。モノレポで複数アプリを抱えている場合は、モノレポ(Monorepo)とは?マルチレポとの違い・メリット・デメリットと導入手順のようにアプリ単位でパッケージを分けたうえで、アプリごとにFSDを適用します。
FSDを採用すべきでない場面と他の設計手法との使い分け
小規模アプリ・短命なプロトタイプ・ライブラリの非採用条件
FSDは全レイヤーの使用を必須とせず、app・pages・sharedから始められます。画面が数枚で配置や依存関係に困っていないアプリでは、移行やPublic API管理の負担を増やしてまで採用する必要はありません。数か月で捨てる検証用のプロトタイプも、ルールを覚えるコストを回収できません。UIライブラリやnpmパッケージはページやルーティングを持たないので、レイヤーの大半が空になります。この3つでは採用しないと決めて構いません。
反対に向くのは、社内の業務システムのように数年単位で機能が増え、担当者が入れ替わるアプリです。業務用・Webアプリ開発のように複数人で保守し続ける前提があるなら、置き場所の議論をルールとリンターに任せられる利点が大きくなります。
Atomic Design・クリーンアーキテクチャとの使い分け
Atomic DesignはUIコンポーネントの粒度の分類で、ロジックや通信の置き場所は決めません。FSDはアプリ全体の置き場所の規約なので、両者は競合せず、shared/uiの中の部品分類にAtomic Designを使う併用ができます。クリーンアーキテクチャやヘキサゴナルアーキテクチャとは?ポートアンドアダプターの仕組みとクリーン・オニオンとの違いは、ビジネスロジックを外部の技術から切り離すための考え方で、主戦場はバックエンドです。フロントエンドで依存の向きを強制したいがレイヤーの数を増やしたくないなら、FSDの方が規約もリンターも揃っていて導入しやすい選択です。
よくある質問
FSDとAtomic Designの違いは何ですか?
Atomic DesignはUIコンポーネントを粒度で分類する手法で、FSDは状態管理や通信も含めたアプリ全体のコードを機能とドメインで配置する規約です。FSDのshared/uiの中でAtomic Designの分類を使う併用もできます。
Processes層は使うべきですか?
使いません。v2.1でprocesses層は非推奨とされ、中身はfeaturesとappへ移すよう公式が求めています。Steigerでもfsd/no-processesがエラーとして検出します。
featuresとentitiesはどう使い分けますか?
ユーザーの操作で、複数のページで再利用されるものがfeatures、userやproductのようにビジネスで使う名詞の概念とその型・取得処理がentitiesです。1ページでしか使わないものは、どちらにも出さずpagesに置いたままにします。
FSDのルールはESLintで強制できますか?
公式チームのリンターはESLintのプラグインではなく、単独で動くSteigerです。ESLintで縛りたい場合はeslint-plugin-boundariesでレイヤーをelementとして定義する方法があります。公式の@feature-sliced/eslint-configはREADMEでWIP(ベータ段階)と案内され、npmの最新版は2024年4月公開の0.1.1のままです。
FSDはReact以外でも使えますか?
使えます。FSDはフォルダ構造とimportの規約なので、Vue・Svelte・Angularなどでも同じレイヤーとスライスを使えます。Next.jsのようにフレームワーク側がappやpagesを予約している場合は、FSDのレイヤー名を_app・_pagesに変えて衝突を避けます。