フロントエンド開発でプロジェクトが拡大するにつれ、「どのファイルがどこにあるか分からない」「依存関係が複雑に絡み合い修正の影響範囲が読めない」といった課題に直面するチームは少なくありません。Feature-Sliced Design(以下FSD)は、こうした構造上の問題を解決するために生まれたアーキテクチャメソドロジーです。

FSDでは Layer(レイヤー)・Slice(スライス)・Segment(セグメント) の3階層でコードを整理し、依存方向を上位から下位への一方向に制限します。2024年11月にはv2.1がリリースされ、Pages Firstアプローチや @x クロスインポート記法など、実務での運用を踏まえた改善が加わりました。

FSDが解決する3つの構造課題

フロントエンドのディレクトリ設計で繰り返し発生する問題は、大きく3つに分類できます。

課題具体的な症状FSDのアプローチ
依存の複雑化モジュール間が双方向に依存し、変更が連鎖するレイヤーの上下関係で依存方向を一方向に制限
コードの散在1つの機能に関連するファイルが複数ディレクトリに分散するスライス単位で機能を凝集させる
暗黙知への依存ディレクトリ命名やファイル配置がチーム内の口伝に頼る標準化された6層構造とPublic APIルールで明文化

FSDはこれら3つを「レイヤーによる依存制御」「スライスによる機能凝集」「Public APIによるカプセル化」で体系的に解決します。

3階層モデル:Layer・Slice・Segment

FSDのプロジェクト構造は3つの階層で構成されます。

src/
├── app/          ← Layer(最上位)
├── pages/        ← Layer
│   ├── home/     ← Slice(ビジネスドメイン単位)
│   │   ├── ui/   ← Segment(技術的関心ごと)
│   │   ├── model/
│   │   └── api/
│   └── settings/
├── widgets/      ← Layer
├── features/     ← Layer
├── entities/     ← Layer
└── shared/       ← Layer(最下位)

Layer はプロジェクト全体を責務ごとに水平に分割する最上位の構造です。各レイヤーは自身より下位のレイヤーからのみインポートでき、上位への依存は禁止されています。

Slice はレイヤー内部をビジネスドメインごとに分割する単位です。userproductcart のようにドメイン名で命名します。App と Shared の2つのレイヤーにはスライスが存在せず、直接セグメントに分割されます。

Segment はスライス内部を技術的な関心ごとに分割する単位です。一般的には ui(コンポーネント)、model(状態・型定義)、api(API通信)、lib(ユーティリティ)、config(設定)の5種類が使われます。

6つのレイヤーの責務と使い分け

FSD v2.1では6つのレイヤー(非推奨の Processes を除く)が定義されています。上から順に並べると次のとおりです。

App — アプリケーション全体の初期化

ルーティング設定、グローバルなプロバイダー(認証・テーマ・i18n)、エントリーポイントなど、アプリケーション起動に必要な設定を配置します。スライスを持たず、セグメントに直接分割されます。

Pages — 画面単位のコンポーネント

ルーティングの各パスに対応するページコンポーネントを配置します。v2.1 の「Pages First」アプローチでは、他のページで再利用しないUI・ロジック・データ取得処理はこのレイヤーに保持することが推奨されています。

Widgets — 自己完結型のUIブロック

ヘッダー、サイドバー、ダッシュボードカードなど、複数のフィーチャーやエンティティを組み合わせて構成される大きなUIブロックを配置します。v2.1 からはウィジェット内に独自のストアやAPIインタラクションを持つことも許可されています。

Features — ユーザーアクションの実装

「いいねボタン」「商品をカートに追加」「コメント投稿フォーム」など、ユーザーに直接価値を提供するアクションの実装を配置します。再利用可能な単位で切り出すのがポイントです。

Entities — ビジネスエンティティの表現

userproductorder など、プロジェクトが扱うビジネスエンティティを配置します。型定義、表示用コンポーネント、エンティティに関連するAPI呼び出しなどが含まれます。

Shared — プロジェクト横断の共有リソース

UIキット、ユーティリティ関数、API クライアント設定、定数など、特定のビジネスドメインに依存しない共有リソースを配置します。v2.1 からは、ルート定数やAPI ベースURL、会社ロゴなどアプリケーション固有のリソースもSharedに配置可能になりました。スライスを持たず、セグメントに直接分割されます。

Processes(非推奨)

v2.1 で正式に非推奨となったレイヤーです。マルチステップ登録フローのように複数ページにまたがるシナリオを扱うために存在していましたが、現在は Features レイヤーへの移行が推奨されています。

依存方向のルールと具体例

FSDの根幹となるルールは「上位レイヤーは下位レイヤーのみをインポートできる」という単方向の依存制約です。

// ✅ OK: features(上位)→ entities(下位)への参照
// src/features/add-to-cart/model/useAddToCart.ts
import { productApi } from '@/entities/product';

// ❌ NG: entities(下位)→ features(上位)への参照
// src/entities/product/ui/ProductCard.tsx
import { AddToCartButton } from '@/features/add-to-cart'; // 禁止

同一レイヤー内のスライス間も原則として相互参照が禁止されています。この制約により、各スライスの独立性が保たれ、変更の影響範囲を局所化できます。

Public APIによるカプセル化

各スライスは index.ts(バレルファイル)を通じてPublic APIを公開します。外部モジュールはこのPublic APIのみを参照し、スライス内部のファイルに直接アクセスしてはなりません。

// src/entities/user/index.ts(Public API)
export { UserCard } from './ui/UserCard';
export { useUser } from './model/useUser';
export type { User } from './model/types';
// 内部のヘルパー関数などはexportしない
// 外部からの参照
// ✅ OK: Public API経由
import { UserCard, useUser } from '@/entities/user';

// ❌ NG: 内部ファイルへの直接参照
import { UserCard } from '@/entities/user/ui/UserCard';

なお、ワイルドカード再エクスポート(export * from ...)は非推奨です。意図しないエクスポートがスライス外部に公開されるリスクがあるためです。

v2.1で導入された @x クロスインポート記法

Entities レイヤーでは、エンティティ間の型参照が避けられないケースがあります。たとえば order エンティティが product の型を参照する場合です。v2.1 では、こうしたクロスインポートを明示的に管理する @x 記法が標準化されました。

src/entities/
├── product/
│   ├── @x/
│   │   └── order.ts    ← order向けに公開する専用Public API
│   ├── ui/
│   ├── model/
│   └── index.ts
└── order/
    ├── model/
    │   └── types.ts    ← ここでproductの型を使用
    └── index.ts
// src/entities/product/@x/order.ts
export type { Product } from '../model/types';

// src/entities/order/model/types.ts
import type { Product } from '@/entities/product/@x/order';

export interface Order {
  id: string;
  items: Product[];
  totalPrice: number;
}

@x は「crossed with」と読み、「product crossed with order」のように理解します。このパターンの利用は Entities レイヤーに限定し、最小限に抑えることが推奨されています。

フロントエンドアーキテクチャの比較

FSDを採用するかどうか判断するうえで、他のアーキテクチャ手法との違いを把握しておくことが重要です。

比較項目FSDAtomic DesignBulletproof Reactフォルダ・バイ・フィーチャー
分割の軸ビジネスドメイン+レイヤー階層UIコンポーネントの粒度機能ディレクトリ機能ディレクトリ
レイヤー数6層(固定)5段階(atoms〜pages)明確な定義なし明確な定義なし
依存方向の制約厳密(上→下の一方向)厳密(小→大の一方向)緩いなし
ロジックの配置各レイヤーのスライス内に凝集UIから分離しにくいfeatures内に集約features内に集約
スケーラビリティ大規模向き中規模まで有効中〜大規模中規模
学習コスト中〜高(独自概念が多い)低〜中
公式ツールSteiger / ESLint Configなしなしなし
コード再利用Public API経由で明示的コンポーネント単位暗黙的暗黙的

Atomic Design はUIコンポーネントの粒度(atoms → molecules → organisms → templates → pages)で分割するため、ビジネスロジックの配置場所が曖昧になりがちです。プロジェクト規模が大きくなると、1つの機能変更で複数のディレクトリを横断する必要が出てきます。

Bulletproof React はfeaturesディレクトリを中心に据える点でFSDと似ていますが、共有コードのルールやレイヤー間の依存方向が厳密に定義されていません。チームの裁量に委ねられる部分が多く、メンバーが増えると一貫性が崩れやすい傾向にあります。

FSD はこれらの手法で課題となる「依存方向の曖昧さ」と「ロジックの散在」を、標準化されたレイヤー構造とPublic APIルールで解決しています。ただし、独自概念(Layer / Slice / Segment / Public API / @x記法)の学習コストが発生する点は考慮が必要です。

Next.jsプロジェクトにFSDを組み込む方法

Next.js App Routerを使ったプロジェクトにFSDを適用する場合、app/ ディレクトリとFSDの app / pages レイヤーで名前が衝突します。公式ドキュメントでは src/ ディレクトリを利用してFSDのレイヤー群をNext.jsのルーティングから分離する方法が推奨されています。

ディレクトリ構成

project-root/
├── app/                     Next.js App Router(ルーティング)
   ├── layout.tsx
   ├── page.tsx
   ├── dashboard/
      └── page.tsx
   └── settings/
       └── page.tsx
├── src/                     FSDのレイヤー群
   ├── app/                 FSD App Layer
      ├── providers/
         └── ThemeProvider.tsx
      └── index.ts
   ├── pages/               FSD Pages Layer
      ├── home/
         ├── ui/
            └── HomePage.tsx
         └── index.ts
      └── dashboard/
          ├── ui/
             └── DashboardPage.tsx
          ├── model/
             └── useDashboardData.ts
          └── index.ts
   ├── widgets/
      └── header/
          ├── ui/
             └── Header.tsx
          └── index.ts
   ├── features/
      └── theme-switcher/
          ├── ui/
             └── ThemeSwitcher.tsx
          ├── model/
             └── useTheme.ts
          └── index.ts
   ├── entities/
      └── user/
          ├── ui/
             └── UserAvatar.tsx
          ├── model/
             └── types.ts
          ├── api/
             └── userApi.ts
          └── index.ts
   └── shared/
       ├── ui/
          ├── Button.tsx
          └── Input.tsx
       ├── api/
          └── client.ts
       ├── lib/
          └── formatDate.ts
       └── config/
           └── constants.ts
└── tsconfig.json

Next.jsのルートファイルからFSDレイヤーを呼び出す

// app/dashboard/page.tsx
import { DashboardPage } from '@/src/pages/dashboard';

export default function Page() {
  return <DashboardPage />;
}
// src/pages/dashboard/ui/DashboardPage.tsx
import { Header } from '@/src/widgets/header';
import { ThemeSwitcher } from '@/src/features/theme-switcher';
import { UserAvatar } from '@/src/entities/user';
import { Button } from '@/src/shared/ui/Button';

export function DashboardPage() {
  return (
    <div>
      <Header />
      <main>
        <UserAvatar />
        <ThemeSwitcher />
        <Button>アクション</Button>
      </main>
    </div>
  );
}
// src/pages/dashboard/index.ts(Public API)
export { DashboardPage } from './ui/DashboardPage';

パスエイリアスの設定

tsconfig.json でパスエイリアスを設定すると、インポートパスが簡潔になります。

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./*"]
    }
  }
}

バレルエクスポートの注意点

Next.js App Routerではバレルファイル(index.ts)経由のインポートがバンドルサイズに影響する場合があります。Next.js 13.5以降で導入された optimizePackageImportsnext.config.js に設定することで、ツリーシェイキングの効率を改善できます。

// next.config.js
module.exports = {
  experimental: {
    optimizePackageImports: ['@/src/shared/ui'],
  },
};

ツールチェーンで依存ルールを自動検証する

FSDのレイヤー間依存ルールを人力で管理するのは現実的ではありません。以下のツールを導入することで、CIや開発中にルール違反を自動検出できます。

Steiger — FSD公式アーキテクチャリンター

Steiger はFSDチームが開発するアーキテクチャリンターです。ファイル構造がFSDの規約に準拠しているかを検査します。

npm install -D steiger @feature-sliced/steiger-plugin
// steiger.config.js
import { defineConfig } from 'steiger';
import fsd from '@feature-sliced/steiger-plugin';

export default defineConfig([
  ...fsd.configs.recommended,
]);
npx steiger src/

Steiger は forbidden-imports(禁止された依存方向のインポート)や insignificant-slice(セグメントが1つしかないスライス)など、FSD固有のルール違反を検出します。

@feature-sliced/eslint-config

ESLintベースでFSDのインポートルールを適用する公式設定です。

npm install -D @feature-sliced/eslint-config
// .eslintrc.js
module.exports = {
  extends: ['@feature-sliced/eslint-config'],
};

eslint-plugin-boundaries

FSD専用ではありませんが、レイヤー間のインポートルールを柔軟に定義できるESLintプラグインです。カスタムルールを細かく設定したい場合に有用です。

npm install -D eslint-plugin-boundaries
// .eslintrc.js(設定例)
module.exports = {
  plugins: ['boundaries'],
  settings: {
    'boundaries/elements': [
      { type: 'app', pattern: 'src/app/*' },
      { type: 'pages', pattern: 'src/pages/*' },
      { type: 'widgets', pattern: 'src/widgets/*' },
      { type: 'features', pattern: 'src/features/*' },
      { type: 'entities', pattern: 'src/entities/*' },
      { type: 'shared', pattern: 'src/shared/*' },
    ],
    'boundaries/ignore': ['**/*.test.*'],
  },
  rules: {
    'boundaries/element-types': [
      'error',
      {
        default: 'disallow',
        rules: [
          { from: 'app', allow: ['pages', 'widgets', 'features', 'entities', 'shared'] },
          { from: 'pages', allow: ['widgets', 'features', 'entities', 'shared'] },
          { from: 'widgets', allow: ['features', 'entities', 'shared'] },
          { from: 'features', allow: ['entities', 'shared'] },
          { from: 'entities', allow: ['shared'] },
          { from: 'shared', allow: [] },
        ],
      },
    ],
  },
};

既存プロジェクトへの段階的な移行手順

FSDを一度にすべてのレイヤーに適用する必要はありません。段階的に導入することで、既存コードへの影響を最小限に抑えられます。

ステップ1:AppとSharedを分離する

まずアプリケーション初期化(プロバイダー、ルーティング設定)を app/ に、共通UIやユーティリティを shared/ に移動します。これだけでもグローバルな設定と再利用可能なコードの境界が明確になります。

ステップ2:PagesとWidgetsを切り出す

ルーティングに対応するページコンポーネントを pages/ に、ヘッダーやサイドバーなど自己完結型のUIブロックを widgets/ に移動します。この段階では残りのコードを暫定的に features/ に配置しておきます。

ステップ3:FeaturesとEntitiesを分離する

ステップ2で features/ に仮置きしたコードから、ビジネスエンティティ(型定義、エンティティ表示コンポーネント)を entities/ に分離します。ユーザーアクションに関わるロジックは features/ に残します。

各ステップの完了後に Steiger や eslint-plugin-boundaries を適用し、依存方向の違反がないことを確認しながら進めると安全です。

v2.1の主な変更点

2024年11月にリリースされたv2.1では、実務でのフィードバックを反映した改善が行われました。v2.0からの破壊的変更はなく、段階的に移行できます。

Pages Firstアプローチ

v2.0ではエンティティやフィーチャーの特定を重視していましたが、v2.1 では「まずページ単位でコードを整理し、再利用が必要になった段階で下位レイヤーに切り出す」というアプローチが推奨されるようになりました。再利用しないUIやロジックは Pages レイヤーに留めることで、コードの凝集性を維持できます。

Processesレイヤーの非推奨化

複数ページにまたがるシナリオを扱う Processes レイヤーは非推奨となりました。該当するロジックは Features レイヤーに移行し、必要に応じて App レイヤーでページ間の連携を補助する設計が推奨されています。

@x クロスインポート記法の標準化

Entities レイヤーでのエンティティ間参照を明示的に管理する @x 記法が正式に標準化されました。従来は暗黙的に行われていた同一レイヤー間のインポートを、専用の Public API を通じて制御できます。

Sharedレイヤーへのアプリ固有コードの許可

v2.0ではSharedにはビジネスドメインに依存しないコードのみを配置するルールでしたが、v2.1ではルート定数、APIベースURL、会社ロゴなどアプリケーション固有のリソースもSharedに配置可能になりました。

FSD導入のメリットと注意点

メリット

  • 変更の影響範囲が明確: レイヤーの依存方向が一方向のため、あるスライスを変更したときに影響を受ける範囲を上位レイヤーに限定できます
  • オンボーディングが速い: 標準化されたディレクトリ構造のため、初めてプロジェクトに参加するメンバーでもコードの配置場所を予測しやすくなります
  • スケーラビリティの確保: KINTOテクノロジーズ社では開発規模が当初の約3倍に拡大しても構造が破綻しなかったと報告されています
  • 自動検証が可能: Steiger や ESLint プラグインにより、依存ルール違反をCIで自動的に検出できます

注意点

  • 学習コスト: Layer / Slice / Segment / Public API / @x 記法など、FSD固有の概念を理解する必要があります
  • index.ts の増加: 各スライスにPublic APIとしてバレルファイルを作成するため、ファイル数が増加します。循環参照やバンドルサイズへの影響に注意が必要です
  • 小規模プロジェクトではオーバースペック: ページ数が数ページ程度の小さなアプリケーションでは、FSDの構造がかえって冗長になる場合があります
  • Segment内の自由度: セグメントの命名やファイル配置にはFSD側の厳密なルールがなく、チーム独自のガイドラインを策定する必要があります

まとめ

Feature-Sliced Designは、フロントエンドプロジェクトの構造を6つのレイヤーで標準化し、依存方向の制約とPublic APIによるカプセル化でコードの見通しを維持するアーキテクチャ手法です。

v2.1では Pages Firstアプローチにより「まずページに集約し、必要に応じて分離する」という実践的な導入フローが確立されました。Next.js App Routerとの組み合わせでは src/ ディレクトリでFSDレイヤーを分離し、Steiger や eslint-plugin-boundaries でルール遵守を自動化することで、チーム規模が拡大しても一貫した設計を保てます。

FSDの公式ドキュメント(feature-sliced.design)には日本語版や実装チュートリアルも用意されています。既存プロジェクトでも段階的な導入が可能なため、ディレクトリ設計に課題を感じている場合は、まず App と Shared の分離から試してみる価値があります。