本記事の執筆にはAIを使用しています。
はじめに
FSD(Feature-Sliced Design)の考え方と、Next.jsへ導入するときのフォルダ構造を紹介します。
レイヤーの分け方だけでなく、依存関係やPublic APIなど、構造を保つためのルールも扱います。
環境
- FSD: 2.1
- Next.js: 16(App Router)
Next.js 15以前を使う場合は、本文の proxy.ts を middleware.ts に読み替えてください。
「FSD」とは?
FSDは、フロントエンドアプリケーションのコードを整理するための設計手法です。
変わり続けるビジネス要件に対して、プロジェクトを理解しやすく安定した状態に保つことを目的としています。
FSDは、フォルダ名をそろえるだけの決まりではありません。
コードを次の3段階に分け、依存できる方向も決めます。
Layers(責務と依存の範囲)
└── Slices(商品や注文などの業務領域)
└── Segments(UIやAPIなどの技術的な役割)
たとえば商品の購入機能は、次のように表せます。
features/ # Layer
└── add-to-cart/ # Slice
├── ui/ # Segment
├── model/ # Segment
├── api/ # Segment
└── index.ts # Public API
features/add-to-cart/ui のように「どの業務の、どの役割か」がパスからわかります。
FSDのメリット
コードを探しやすい
レイヤーの意味が共通なので、初めて触るプロジェクトでもコードの場所を予想しやすいです。
スライスには業務で使う言葉を付けるため、機能とコードの対応も見つけやすくなります。
変更の影響を狭めやすい
依存方向が決まっており、同じレイヤーのスライス同士は原則として依存しません。
あるスライスを変更したときに、無関係なスライスまで壊すリスクを減らせます。
再利用する範囲を調整しやすい
ページ固有のコードは上位レイヤーへ置き、広く使うコードは下位レイヤーへ置きます。
何でも共通化せず、必要になった時点で再利用しやすい場所へ移せます。
チーム内で設計を話しやすい
「これは features か entities か」「このコードは同じスライスに置くか」のように、共通の言葉で設計を話せます。
FSDのデメリット
覚えるルールが増える
レイヤーごとの責務、依存方向、Public APIの扱いをチームで理解する必要があります。
導入直後は、コードをどこへ置くか迷いやすいです。
小規模なアプリでは構造が重くなる
画面や機能が少ないアプリを細かく分けると、フォルダと index.ts ばかり増えます。
必要なレイヤーだけを使い、再利用されない処理を無理に切り出さないことが大切です。
境界の判断に正解がない
同じUIでも、ページ固有なら pages、複数ページで使う操作なら features、業務知識を持たない部品なら shared が候補です。
プロダクトやチームによって適切な境界は変わります。
ルールはフォルダだけでは守れない
深いパスへの直接importや、禁止された依存はTypeScriptだけでは防げません。
コードレビューやアーキテクチャリンターが必要です。
Layers・Slices・Segments
Layers
Layersは、コードを責務と依存の広さで分ける最上位の区分です。
上から順に、次の7層があります。
| レイヤー | 役割 | 例 |
|---|---|---|
app |
アプリ全体の設定 | ルーティング、Provider、グローバルスタイル、計測 |
processes |
複数ページにまたがる処理 | 現在は非推奨 |
pages |
画面全体、またはネストしたルートの大きな単位 | 商品一覧、商品詳細、ログイン |
widgets |
再利用する大きなUIブロック | 現在は原則として利用非推奨 |
features |
ユーザーに価値を与える、複数ページで再利用する操作 | カートへ追加、検索、ログイン |
entities |
プロダクトが扱う業務上の対象 | 商品、ユーザー、注文 |
shared |
業務に依存しない共通の土台 | UIキット、APIクライアント、設定 |
すべてのレイヤーを使う必要はありません。
一般的には app、pages、shared から始めます。
processes は非推奨です。
複数ページにまたがる処理は、まず features や app に置けないか検討します。
現在の公式ドキュメントは、widgets の利用も原則として勧めていません。
画面固有のまとまりは pages、複数ページで再利用する操作とUIは features、業務に依存しないUIは shared に置くと境界が明確です。
レイヤーの依存方向
コードは、自分より下のレイヤーだけをimportできます。
app
↓
pages
↓
widgets
↓
features
↓
entities
↓
shared
たとえば features は entities と shared を使えます。
反対に、entities から features は使えません。
同じレイヤーにある別スライスのimportも原則として禁止です。
features/add-to-cart から features/remove-from-cart を使いたくなったら、共通部分を下位レイヤーへ移すか、上位レイヤーで組み合わせます。
app と shared は例外です。
この2層にはスライスがなく、直下のセグメント同士でimportできます。
Slices
Slicesは、レイヤーを業務領域で分ける単位です。
名前はFSDで決められておらず、プロダクトの言葉を使います。
entities/
├── product/
├── user/
└── order/
features/
├── add-to-cart/
├── search-products/
└── sign-in/
同じレイヤーのスライスは、互いに独立させます。
関連するコードを1か所へ集めつつ、別のスライスとの結び付きを弱くするためです。
app と shared には業務領域がないため、スライスを作りません。
Segments
Segmentsは、スライス内のコードを技術的な役割で分ける単位です。
| セグメント | 役割 |
|---|---|
ui |
コンポーネント、表示用の整形、スタイル |
api |
APIリクエスト、レスポンス型、データ変換 |
model |
型、スキーマ、状態、ビジネスロジック |
lib |
そのスライス内で使う小さなライブラリ |
config |
設定やFeature Flag |
必要なセグメントだけを作ります。
components、hooks、types のようにファイルの種類で分けるより、ui、model、api のように目的で分けると探しやすくなります。
Next.jsでのフォルダ構造
App Routerを使うECサイトを例にします。
.
├── app/ # Next.jsのルーティング
│ ├── layout.tsx
│ ├── page.tsx
│ ├── products/
│ │ └── [productId]/
│ │ └── page.tsx
│ └── api/
│ └── products/
│ └── route.ts
├── src/
│ ├── _app/ # FSDのAppレイヤー
│ │ ├── providers/
│ │ ├── styles/
│ │ └── api-routes/
│ ├── _pages/ # FSDのPagesレイヤー
│ │ ├── home/
│ │ │ ├── ui/
│ │ │ │ └── HomePage.tsx
│ │ │ └── index.ts
│ │ └── product-details/
│ │ ├── api/
│ │ ├── ui/
│ │ └── index.ts
│ ├── features/
│ │ └── add-to-cart/
│ │ ├── model/
│ │ ├── ui/
│ │ └── index.ts
│ ├── entities/
│ │ └── product/
│ │ ├── model/
│ │ ├── ui/
│ │ └── index.ts
│ └── shared/
│ ├── api/
│ ├── config/
│ ├── lib/
│ └── ui/
├── instrumentation.ts
├── proxy.ts
└── tsconfig.json
Next.jsとFSDの app・pages を区別する
Next.jsもFSDも app と pages という名前を使います。
公式ガイドは、FSD側を src/_app と src/_pages に変える方法を勧めています。
Next.jsの app はプロジェクトルートへ置き、ルーティングに必要な薄いファイルだけを残します。
画面の実装は src/_pages から公開します。
export { ProductDetailsPage as default, generateMetadata } from "@/_pages/product-details/index.server";
@ を src へ向けます。
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
Pages Routerを使う場合も同じです。
Next.jsの pages をルートへ置き、FSD側は src/_pages にします。
Server ComponentとClient Componentを混ぜない
App Routerでは、1つのスライスにServer ComponentとClient Componentが入ることがあります。
サーバー専用のモジュールを通常の index.ts から公開すると、Client Componentの依存へ混ざり、ビルドエラーにつながります。
サーバー専用の公開口は index.server.ts に分けます。
export { AddToCartButton } from "./ui/AddToCartButton";
import "server-only";
export { ProductDetailsPage } from "./ui/ProductDetailsPage";
export { getProduct } from "./api/getProduct";
export { generateMetadata } from "./lib/generateMetadata";
Client Componentから index.server.ts をimportしないようにします。
Route Handlerは薄く保つ
Route Handlerの実装は、FSDの src/_app/api-routes に置けます。
Next.js側の route.ts は公開された関数を再exportします。
export { getProducts as GET } from "@/_app/api-routes";
FSDはフロントエンド向けの設計手法です。
バックエンドの処理が大きくなるなら、モノレポ内の別パッケージへ分けることも検討します。
proxy.ts と instrumentation.ts は、Next.jsの決まりに従ってプロジェクトルートへ置きます。
Next.js 16では middleware.ts が非推奨となり、proxy.ts に名前が変わりました。
FSDで知っておくべきルール
スライスの外からはPublic APIを使う
各スライスは、外部へ公開するものを index.ts に明示します。
export { AddToCartButton } from "./ui/AddToCartButton";
export { useCart } from "./model/useCart";
別のスライスから内部のファイルを直接importしません。
// ⭕ Public APIからimportする
import { AddToCartButton } from "@/features/add-to-cart";
// ❌ スライスの内部へ直接アクセスする
import { AddToCartButton } from "@/features/add-to-cart/ui/AddToCartButton";
export * は避け、必要なものだけを公開します。
スライスの内部構造を変えても、利用側への影響を抑えられます。
ただし、同じスライス内ではPublic APIを経由しません。
相対パスで直接importすると、index.ts を介した循環参照を防げます。
// src/features/add-to-cart/ui/AddToCartButton.tsx
import { useCart } from "../model/useCart";
shared/ui と shared/lib は内容が増えやすいため、全体を1つの index.ts にまとめないほうが安全です。
コンポーネントやライブラリごとにPublic APIを作ります。
import { Button } from "@/shared/ui/button";
import { formatPrice } from "@/shared/lib/price";
同じレイヤーのcross-importを避ける
同じレイヤーにある別スライスへの依存は、スライスの独立性を壊します。
まずは次のどちらかで解消します。
- 共通する処理を下位レイヤーへ移す
- 両方を使える上位レイヤーで組み合わせる
業務上の関係により、entities 同士の参照を避けられないことがあります。
その場合は @x 記法で、相手専用のPublic APIを作れます。
entities/
├── artist/
│ └── model/artist.ts
└── song/
├── @x/
│ └── artist.ts
└── model/song.ts
import type { Song } from "@/entities/song/@x/artist";
@x は例外です。
cross-importを増やすために使わず、entities で必要な場合だけに絞ります。
先に分けすぎない
FSD 2.1では、まずコードを pages と shared に置く考え方が勧められています。
複数ページで再利用する業務ロジックが見つかったら、features や entities へ切り出します。
1つのページでしか使わないフォームを、最初から features にする必要はありません。
小さなスライスが増えすぎると、重要な機能を探しにくくなります。
判断に迷ったら、現在使う場所へ近い上位レイヤーに置きます。
実際に再利用されてから下位レイヤーへ移すと、早すぎる共通化を防げます。
APIを置く場所は利用範囲で決める
共通のAPIクライアントは shared/api に置きます。
特定のページだけで使うリクエストは、そのページの api セグメントに置けます。
APIレスポンスと画面で扱うEntityが同じ形とは限りません。
レスポンス型やリクエストを、早い段階ですべて entities へ集めないようにします。
ルールを自動で検査する
フォルダ構造だけでは、依存方向やPublic API違反を防げません。
公式のアーキテクチャリンターであるSteigerを使うと、FSDのルール違反や細かすぎるスライスを検出できます。
$ npx steiger src
CIでも実行すると、プロジェクトが大きくなっても構造を保ちやすいです。
既存プロジェクトへ段階的に導入する
最初からすべてを移動する必要はありません。
公式ドキュメントでは、次の順番が案内されています。
-
appとsharedを少しずつ整える - 既存のUIを大まかに
pagesへ移す - import違反を直しながら、必要な
entitiesとfeaturesを切り出す
Next.jsでは、最初にルーティングと画面実装を分けるだけでも効果があります。
変更する機能から少しずつ移すと、動作確認の範囲も狭くできます。
おわりに
FSDの中心にあるのは、業務に沿ってコードをまとめ、依存する方向をそろえる考え方です。
すべてのレイヤーを最初から用意せず、まずは app、pages、shared から始めるのがよいです。
再利用したい処理が見つかったら、少しずつ features や entities へ切り出してみてください ![]()
参考リンク
- https://fsd.how/docs/reference/layers/
- https://fsd.how/docs/reference/slices-segments/
- https://fsd.how/docs/reference/public-api/
- https://fsd.how/docs/guides/migration/from-v2-0/
- https://fsd.how/docs/guides/tech/with-nextjs/
- https://fsd.how/docs/guides/examples/api-requests/
- https://nextjs.org/docs/app/getting-started/project-structure
- https://nextjs.org/docs/app/api-reference/file-conventions/proxy