0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

FSD(Feature-Sliced Design)の基本とNext.jsでの使い方

0
Posted at

本記事の執筆にはAIを使用しています。

はじめに

FSD(Feature-Sliced Design)の考え方と、Next.jsへ導入するときのフォルダ構造を紹介します。
レイヤーの分け方だけでなく、依存関係やPublic APIなど、構造を保つためのルールも扱います。

環境

  • FSD: 2.1
  • Next.js: 16(App Router)

Next.js 15以前を使う場合は、本文の proxy.tsmiddleware.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のメリット

コードを探しやすい

レイヤーの意味が共通なので、初めて触るプロジェクトでもコードの場所を予想しやすいです。
スライスには業務で使う言葉を付けるため、機能とコードの対応も見つけやすくなります。

変更の影響を狭めやすい

依存方向が決まっており、同じレイヤーのスライス同士は原則として依存しません。
あるスライスを変更したときに、無関係なスライスまで壊すリスクを減らせます。

再利用する範囲を調整しやすい

ページ固有のコードは上位レイヤーへ置き、広く使うコードは下位レイヤーへ置きます。
何でも共通化せず、必要になった時点で再利用しやすい場所へ移せます。

チーム内で設計を話しやすい

「これは featuresentities か」「このコードは同じスライスに置くか」のように、共通の言葉で設計を話せます。

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クライアント、設定

すべてのレイヤーを使う必要はありません。
一般的には apppagesshared から始めます。

processes は非推奨です。
複数ページにまたがる処理は、まず featuresapp に置けないか検討します。

現在の公式ドキュメントは、widgets の利用も原則として勧めていません。
画面固有のまとまりは pages、複数ページで再利用する操作とUIは features、業務に依存しないUIは shared に置くと境界が明確です。

レイヤーの依存方向

コードは、自分より下のレイヤーだけをimportできます。

app
  ↓
pages
  ↓
widgets
  ↓
features
  ↓
entities
  ↓
shared

たとえば featuresentitiesshared を使えます。
反対に、entities から features は使えません。

同じレイヤーにある別スライスのimportも原則として禁止です。
features/add-to-cart から features/remove-from-cart を使いたくなったら、共通部分を下位レイヤーへ移すか、上位レイヤーで組み合わせます。

appshared は例外です。
この2層にはスライスがなく、直下のセグメント同士でimportできます。

Slices

Slicesは、レイヤーを業務領域で分ける単位です。
名前はFSDで決められておらず、プロダクトの言葉を使います。

entities/
├── product/
├── user/
└── order/

features/
├── add-to-cart/
├── search-products/
└── sign-in/

同じレイヤーのスライスは、互いに独立させます。
関連するコードを1か所へ集めつつ、別のスライスとの結び付きを弱くするためです。

appshared には業務領域がないため、スライスを作りません。

Segments

Segmentsは、スライス内のコードを技術的な役割で分ける単位です。

セグメント 役割
ui コンポーネント、表示用の整形、スタイル
api APIリクエスト、レスポンス型、データ変換
model 型、スキーマ、状態、ビジネスロジック
lib そのスライス内で使う小さなライブラリ
config 設定やFeature Flag

必要なセグメントだけを作ります。
componentshookstypes のようにファイルの種類で分けるより、uimodelapi のように目的で分けると探しやすくなります。

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の apppages を区別する

Next.jsもFSDも apppages という名前を使います。
公式ガイドは、FSD側を src/_appsrc/_pages に変える方法を勧めています。

Next.jsの app はプロジェクトルートへ置き、ルーティングに必要な薄いファイルだけを残します。
画面の実装は src/_pages から公開します。

app/products/[productId]/page.tsx
export { ProductDetailsPage as default, generateMetadata } from "@/_pages/product-details/index.server";

@src へ向けます。

tsconfig.json
{
  "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 に分けます。

src/_pages/product-details/index.ts
export { AddToCartButton } from "./ui/AddToCartButton";
src/_pages/product-details/index.server.ts
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します。

app/api/products/route.ts
export { getProducts as GET } from "@/_app/api-routes";

FSDはフロントエンド向けの設計手法です。
バックエンドの処理が大きくなるなら、モノレポ内の別パッケージへ分けることも検討します。

proxy.tsinstrumentation.ts は、Next.jsの決まりに従ってプロジェクトルートへ置きます。
Next.js 16では middleware.ts が非推奨となり、proxy.ts に名前が変わりました。

FSDで知っておくべきルール

スライスの外からはPublic APIを使う

各スライスは、外部へ公開するものを index.ts に明示します。

src/features/add-to-cart/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/uishared/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
src/entities/artist/model/artist.ts
import type { Song } from "@/entities/song/@x/artist";

@x は例外です。
cross-importを増やすために使わず、entities で必要な場合だけに絞ります。

先に分けすぎない

FSD 2.1では、まずコードを pagesshared に置く考え方が勧められています。
複数ページで再利用する業務ロジックが見つかったら、featuresentities へ切り出します。

1つのページでしか使わないフォームを、最初から features にする必要はありません。
小さなスライスが増えすぎると、重要な機能を探しにくくなります。

判断に迷ったら、現在使う場所へ近い上位レイヤーに置きます。
実際に再利用されてから下位レイヤーへ移すと、早すぎる共通化を防げます。

APIを置く場所は利用範囲で決める

共通のAPIクライアントは shared/api に置きます。
特定のページだけで使うリクエストは、そのページの api セグメントに置けます。

APIレスポンスと画面で扱うEntityが同じ形とは限りません。
レスポンス型やリクエストを、早い段階ですべて entities へ集めないようにします。

ルールを自動で検査する

フォルダ構造だけでは、依存方向やPublic API違反を防げません。
公式のアーキテクチャリンターであるSteigerを使うと、FSDのルール違反や細かすぎるスライスを検出できます。

$ npx steiger src

CIでも実行すると、プロジェクトが大きくなっても構造を保ちやすいです。

既存プロジェクトへ段階的に導入する

最初からすべてを移動する必要はありません。
公式ドキュメントでは、次の順番が案内されています。

  1. appshared を少しずつ整える
  2. 既存のUIを大まかに pages へ移す
  3. import違反を直しながら、必要な entitiesfeatures を切り出す

Next.jsでは、最初にルーティングと画面実装を分けるだけでも効果があります。
変更する機能から少しずつ移すと、動作確認の範囲も狭くできます。

おわりに

FSDの中心にあるのは、業務に沿ってコードをまとめ、依存する方向をそろえる考え方です。
すべてのレイヤーを最初から用意せず、まずは apppagesshared から始めるのがよいです。
再利用したい処理が見つかったら、少しずつ featuresentities へ切り出してみてください :relaxed:

参考リンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?