mcpField
F
Feature-Sliced Design

Feature-Sliced Design

github.com/constellos開発0 回インストール
リポジトリ

Feature-Sliced Design について

Feature-Sliced Designは、レイヤー、スライス、セグメントで拡張可能なアプリを整理するフロントエンド向け設計手法のスキルです。独自の`views`レイヤーにより、ページロジックとNext.js App Routerのルーティングを分離します。

主な機能

  1. 017層のアーキテクチャ
  2. 02レイヤーに基づくインポート規則
  3. 03ドメイン別スライスと公開API
  4. 04用途別のコードセグメント
  5. 05Next.js App Routerの構成
  6. 06Turborepoのアプリ構成ガイド

活用シーン

  1. 01新しいNext.jsアプリの構成
  2. 02成長したフロントエンドコードの再編
  3. 03チームでの並行開発の調整
  4. 04Turborepo内の独立したアプリの整理

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/constellos/claude-code --skill feature-sliced-design

インストール後、Claude Code で/feature-sliced-designを入力して読み込まれたか確認します。

詳細

Feature-Sliced Design

目的

Feature-Sliced Design(FSD)は、フロントエンドアプリケーションを標準化されたスケーラブルな構造に整理するためのアーキテクチャ手法です。階層構造によって関心事を明確に分離し、循環依存を防ぎ、保守性を高めます。

FSD を使用する理由:

  • スケーラビリティ:アプリケーションの拡大に合わせて自然に成長します
  • 保守性:境界が明確なため、安全にリファクタリングできます
  • チームでの協業:一貫性のある構造により、並行開発が可能になります
  • オンボーディング:新しい開発者がアーキテクチャをすぐに理解できます

カスタムの「views」レイヤー: このスキルでは、Next.js App Router の /app ディレクトリとの混同を避けるため、標準 FSD の「pages」レイヤーの代わりに「views」を使用します。/app ディレクトリはルーティングのみを処理し(ロジックは最小限)、/src/views には実際のページのビジネスロジックを配置します。

Next.js との統合: FSD は、ルーティング関連の処理(/app 内)とビジネスロジック(/src/views およびその他の FSD レイヤー内)を分離することで、Next.js App Router とシームレスに連携します。これにより、FSD のアーキテクチャ上の利点を維持しながら、ルーティング設定をすっきり保てます。

使用する場面

次のような場合に Feature-Sliced Design を適用してください:

  • 明確なアーキテクチャ上の境界が必要な、新しい Next.js プロジェクトを開始するとき
  • 一貫した構造がない、拡大中のコードベースをリファクタリングするとき
  • 標準化された構成を必要とする複数の開発者から成るチームで作業するとき
  • 関心事の分離が必要な、複雑なビジネスロジックを持つアプリケーションを構築するとき
  • 各アプリに独立した FSD 構造が必要な Turborepo モノレポアプリケーションを開発するとき
  • 循環依存が問題になり始めたアプリケーションをスケールするとき
  • 長期的な保守が必要なエンタープライズアプリケーションを作成するとき

基本原則

レイヤー階層

FSD では、コードを標準化された 7 つのレイヤー(上位から下位の順)に整理します:

  1. app - アプリケーションの初期化、グローバルプロバイダー、ルーティング設定
  2. processes - 非推奨(機能は features と app に移行)
  3. views - ページレベルのビジネスロジック(カスタムの命名で、標準の「pages」を置き換えます)
  4. widgets - 複数の機能にまたがる大規模な複合 UI ブロック
  5. features - ビジネス上の価値を持つ、ユーザー向けの操作
  6. entities - ビジネスドメインのオブジェクトとモデル
  7. shared - 再利用可能なユーティリティ、UI キット、サードパーティとの連携

インポートルール: モジュールがインポートできるのは、階層内で自分より厳密に下位のレイヤーのみです。

┌─────────────────┐
│      app        │  ← Can import from all layers below
├─────────────────┤
│     views       │  ← Can import: widgets, features, entities, shared
├─────────────────┤
│    widgets      │  ← Can import: features, entities, shared
├─────────────────┤
│    features     │  ← Can import: entities, shared
├─────────────────┤
│    entities     │  ← Can import: shared only
├─────────────────┤
│     shared      │  ← Cannot import from any FSD layer
└─────────────────┘

この階層によって循環依存を防ぎ、アーキテクチャ上の境界を明確に保てます。

「views」レイヤーと「pages」レイヤー

「pages」ではなく「views」を使用する理由:

  • Next.js ではルーティングに /app ディレクトリを使用します(App Router)
  • 標準 FSD では、ページのビジネスロジックに「pages」レイヤーを使用します
  • 「views」を使うことで、ルーティング(/app)とビジネスロジック(/src/views)の混同を避けられます

関心事の分離:

  • /app ディレクトリ(ルートレベル):Next.js のルーティングのみを扱い、ロジックは最小限にします
    • page.tsx、layout.tsx、ルートグループを含みます
    • /src/views からインポートしてレンダリングします
  • /src/views レイヤー(FSD):ページのビジネスロジックとコンポーネントの構成を担います
    • ビューコンポーネント、モデル、API 呼び出しを含みます
    • ウィジェット、機能、エンティティを組み合わせます

この分離により、FSD のアーキテクチャ原則を維持しながら、ルーティング設定をすっきり保てます。

スライス

スライスは、レイヤー内のドメインベースの区分です(スライスを持たない app と shared を除きます)。

例:

  • views/dashboard - ダッシュボードページのスライス
  • widgets/header - ヘッダーウィジェットのスライス
  • features/auth - 認証機能のスライス
  • entities/user - ユーザーエンティティのスライス

パブリック API パターン: 各スライスは index.ts を通じてエクスポートし、公開インターフェースを制御します:

// src/features/auth/index.ts
export { LoginForm } from './ui/LoginForm';
export { useAuth } from './model/useAuth';
export type { AuthState } from './model/types';
// Internal implementation details NOT exported

これにより、深い階層からのインポートを防ぎ、カプセル化を維持できます。

セグメント

セグメントは、スライス内で目的別に分けられたグループです。

  • ui/ - React コンポーネント、視覚要素
  • model/ - ビジネスロジック、状態管理、TypeScript 型
  • api/ - API クライアント、データ取得、外部連携
  • lib/ - スライス固有のユーティリティ関数、ヘルパー
  • config/ - 設定定数、機能フラグ

構成例:

features/
└── auth/
    ├── ui/
    │   ├── LoginForm.tsx
    │   └── SignupForm.tsx
    ├── model/
    │   ├── useAuth.ts
    │   └── types.ts
    ├── api/
    │   └── authApi.ts
    └── index.ts

Next.js App Router での FSD

ルーティングアーキテクチャ

Next.js App Router は、ルーティングに /app ディレクトリを使用します。FSD のレイヤーは /src ディレクトリに配置します。

ファイル構成:

my-nextjs-app/
├── app/                          # Next.js routing (minimal logic)
│   ├── layout.tsx               # Root layout
│   ├── page.tsx                 # Home route
│   ├── dashboard/
│   │   └── page.tsx            # Dashboard route
│   └── settings/
│       └── page.tsx            # Settings route
│
├── src/                         # FSD layers
│   └── views/                  # Page business logic
│       ├── home/
│       │   ├── ui/
│       │   │   └── HomeView.tsx
│       │   └── index.ts
│       ├── dashboard/
│       │   ├── ui/
│       │   │   └── DashboardView.tsx
│       │   ├── model/
│       │   │   └── useDashboard.ts
│       │   └── index.ts
│       └── settings/
│           ├── ui/
│           │   └── SettingsView.tsx
│           └── index.ts

ルーティングページからビューをインポートします:

// app/dashboard/page.tsx - Routing only
import { DashboardView } from '@/views/dashboard';

export default function DashboardPage() {
  return <DashboardView />;
}

// src/views/dashboard/ui/DashboardView.tsx - Business logic
import { Header } from '@/widgets/header';
import { StatsCard } from '@/features/analytics';

export function DashboardView() {
  return (
    <div>
      <Header />
      <StatsCard />
    </div>
  );
}
単体の Next.js 構成

単体の Next.js アプリケーションにおける完全な FSD 構成:

my-nextjs-app/
├── app/                          # Next.js App Router
│   ├── layout.tsx               # Root layout
│   ├── page.tsx                 # Home route
│   ├── (auth)/                  # Route group
│   │   ├── login/
│   │   │   └── page.tsx
│   │   └── signup/
│   │       └── page.tsx
│   ├── dashboard/
│   │   └── page.tsx
│   ├── api/                     # API routes
│   │   └── users/
│   │       └── route.ts
│   └── not-found.tsx
│
├── src/
│   ├── app/                     # App layer (no slices)
│   │   ├── providers/
│   │   │   ├── AuthProvider.tsx
│   │   │   └── QueryProvider.tsx
│   │   ├── styles/
│   │   │   └── globals.css
│   │   └── config/
│   │       └── constants.ts
│   │
│   ├── views/                   # Views layer (page logic)
│   │   ├── home/
│   │   ├── dashboard/
│   │   ├── login/
│   │   └── signup/
│   │
│   ├── widgets/                 # Widgets layer
│   │   ├── header/
│   │   ├── sidebar/
│   │   ├── footer/
│   │   └── notification-panel/
│   │
│   ├── features/                # Features layer
│   │   ├── auth/
│   │   ├── search/
│   │   ├── theme-toggle/
│   │   └── user-profile/
│   │
│   ├── entities/                # Entities layer
│   │   ├── user/
│   │   ├── post/
│   │   ├── comment/
│   │   └── session/
│   │
│   └── shared/                  # Shared layer (no slices)
│       ├── ui/                  # UI components
│       │   ├── button/
│       │   ├── input/
│       │   └── card/
│       ├── lib/                 # Utilities
│       │   ├── format.ts
│       │   └── validation.ts
│       ├── api/                 # API client
│       │   └── client.ts
│       └── config/
│           └── env.ts
│
├── public/
│   ├── images/
│   └── fonts/
│
└── package.json
Turborepo モノレポ構成

各アプリが独立した FSD 構成を持つ、Turborepo モノレポ内の FSD 構成:

turborepo-root/
├── apps/
│   ├── web/                     # Consumer-facing app
│   │   ├── app/                # Next.js routing
│   │   │   ├── layout.tsx
│   │   │   ├── page.tsx
│   │   │   └── shop/
│   │   │       └── page.tsx
│   │   ├── src/                # Independent FSD structure
│   │   │   ├── app/
│   │   │   ├── views/
│   │   │   │   ├── home/
│   │   │   │   └── shop/
│   │   │   ├── widgets/
│   │   │   │   ├── product-grid/
│   │   │   │   └── shopping-cart/
│   │   │   ├── features/
│   │   │   │   ├── add-to-cart/
│   │   │   │   └── checkout/
│   │   │   ├── entities/
│   │   │   │   ├── product/
│   │   │   │   └── order/
│   │   │   └── shared/
│   │   └── package.json
│   │
│   └── admin/                   # Admin dashboard app
│       ├── app/                # Next.js routing
│       │   ├── layout.tsx
│       │   ├── page.tsx
│       │   └── products/
│       │       └── page.tsx
│       ├── src/                # Independent FSD structure
│       │   ├── app/
│       │   ├── views/
│       │   │   ├── dashboard/
│       │   │   └── products/
│       │   ├── widgets/
│       │   │   ├── admin-header/
│       │   │   └── stats-panel/
│       │   ├── features/
│       │   │   ├── product-editor/
│       │   │   └── user-management/
│       │   ├── entities/
│       │   │   ├── product/
│       │   │   └── admin/
│       │   └── shared/
│       └── package.json
│
├── packages/                    # Optional shared packages
│   ├── ui/                     # Shared UI components (can mirror shared/ui)
│   │   ├── button/
│   │   └── input/
│   ├── utils/                  # Shared utilities
│   │   └── validation.ts
│   └── types/                  # Shared TypeScript types
│       └── common.ts
│
├── turbo.json
└── package.json

Turborepo の主な原則:

  • 各アプリ(web、admin)は、それぞれ独自の完全な FSD 構成を持ちます
  • アプリは独立しており、FSD レイヤー間でアプリをまたいだインポートは行いません
  • 共有コードは packages/ ディレクトリに配置します(任意)
  • パッケージ依存関係には workspace:* プロトコルを使用します

レイヤーの定義

app レイヤー

目的: アプリケーション全体のセットアップ、初期化、グローバル設定を行います。

役割:

  • グローバルプロバイダー(テーマ、認証、クエリクライアント)
  • ルートスタイルと CSS のインポート
  • アプリケーションレベルの設定
  • エラーバウンダリー

インポートルール: 下位のすべてのレイヤー(views、widgets、features、entities、shared)からインポートできます。

スライスなし: app レイヤーにはセグメントを直接配置します(providers/、styles/、config/)。

例:

// src/app/providers/Providers.tsx
'use client';

import { QueryClientProvider } from '@tanstack/react-query';
import { queryClient } from '@/shared/api/queryClient';
import { AuthProvider } from '@/features/auth';

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <QueryClientProvider client={queryClient}>
      <AuthProvider>
        {children}
      </AuthProvider>
    </QueryClientProvider>
  );
}

// app/layout.tsx
import { Providers } from '@/app/providers/Providers';
import '@/app/styles/globals.css';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}
views レイヤー

目的: ページレベルのビジネスロジックとコンポーネントの構成を担います。

役割:

  • ウィジェット、機能、エンティティを組み合わせて完全なページを構成します
  • ページ固有の状態管理
  • ページのデータ取得
  • SEO メタデータ

インポートルール: widgets、features、entities、shared からインポートできます。

スライスあり: ページごとに独自のスライスを持ちます(例:views/dashboard、views/settings)。

例:

// src/views/dashboard/ui/DashboardView.tsx
import { Header } from '@/widgets/header';
import { Sidebar } from '@/widgets/sidebar';
import { StatsCard } from '@/features/analytics';
import { RecentActivity } from '@/features/activity';
import { User } from '@/entities/user';

interface DashboardViewProps {
  user: User;
}

export function DashboardView({ user }: DashboardViewProps) {
  return (
    <div className="dashboard">
      <Header user={user} />
      <div className="dashboard-content">
        <Sidebar />
        <main>
          <StatsCard userId={user.id} />
          <RecentActivity userId={user.id} />
        </main>
      </div>
    </div>
  );
}

// src/views/dashboard/index.ts
export { DashboardView } from './ui/DashboardView';

// app/dashboard/page.tsx
import { DashboardView } from '@/views/dashboard';
import { getCurrentUser } from '@/entities/user';

export default async function DashboardPage() {
  const user = await getCurrentUser();
  return <DashboardView user={user} />;
}
widgets レイヤー

目的: 複数の機能を組み合わせた、大規模で独立した複合 UI ブロックです。

役割:

  • 複数のページで再利用できます
  • 複数の機能を組み合わせます
  • 複雑な UI レイアウト(ヘッダー、サイドバー、フッター)
  • ナビゲーションコンポーネント

インポートルール: features、entities、shared からインポートできます。

スライスあり: ウィジェットごとに独自のスライスを持ちます(例:widgets/header、widgets/sidebar)。

例:

// src/widgets/header/ui/Header.tsx
import { SearchBar } from '@/features/search';
import { UserMenu } from '@/features/user-menu';
import { NotificationBell } from '@/features/notifications';
import { User } from '@/entities/user';
import { Logo } from '@/shared/ui/logo';

interface HeaderProps {
  user: User;
}

export function Header({ user }: HeaderProps) {
  return (
    <header className="header">
      <Logo />
      <SearchBar />
      <div className="header-actions">
        <NotificationBell userId={user.id} />
        <UserMenu user={user} />
      </div>
    </header>
  );
}

// src/widgets/header/index.ts
export { Header } from './ui/Header';
features レイヤー

目的: 明確なビジネス価値を持つ、ユーザー向けの操作とビジネスロジックを担います。

役割:

  • 特定のユーザー操作(ログイン、カートへの追加、投稿への「いいね」)
  • 機能固有の状態管理
  • ビジネスロジックと検証
  • 機能の API 操作

インポートルール: entities、shared からインポートできます。

スライスあり: 機能ごとに独自のスライスを持ちます(例:features/auth、features/search)。

例:

// src/features/auth/model/types.ts
export interface LoginCredentials {
  email: string;
  password: string;
}

// src/features/auth/api/login.ts
import { User } from '@/entities/user';
import { apiClient } from '@/shared/api/client';
import type { LoginCredentials } from '../model/types';

export async function login(credentials: LoginCredentials): Promise<User> {
  const response = await apiClient.post('/auth/login', credentials);
  return response.data;
}

// src/features/auth/ui/LoginForm.tsx
'use client';

import { useState } from 'react';
import { login } from '../api/login';
import { Button } from '@/shared/ui/button';
import { Input } from '@/shared/ui/input';

export function LoginForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    await login({ email, password });
  };

  return (
    <form onSubmit={handleSubmit}>
      <Input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
      />
      <Input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Password"
      />
      <Button type="submit">Login</Button>
    </form>
  );
}

// src/features/auth/index.ts
export { LoginForm } from './ui/LoginForm';
export { login } from './api/login';
export type { LoginCredentials } from './model/types';
entities レイヤー

目的: ビジネスドメインのオブジェクトと中核となるデータモデルを扱います。

役割:

  • ビジネス概念を表すデータ構造
  • エンティティ固有のユーティリティ
  • 基本的な API 操作(CRUD)
  • 型定義

インポートルール: shared のみからインポートできます。

スライスあり: エンティティごとに独自のスライスを持ちます(例:entities/user、entities/post)。

例:

// src/entities/user/model/types.ts
export interface User {
  id: string;
  name: string;
  email: string;
  avatar?: string;
  role: 'admin' | 'user';
}

// src/entities/user/api/getUser.ts
import { apiClient } from '@/shared/api/client';
import type { User } from '../model/types';

export async function getUser(id: string): Promise<User> {
  const response = await apiClient.get(`/users/${id}`);
  return response.data;
}

export async function getCurrentUser(): Promise<User> {
  const response = await apiClient.get('/users/me');
  return response.data;
}

// src/entities/user/ui/UserCard.tsx
import type { User } from '../model/types';
import { Avatar } from '@/shared/ui/avatar';

interface UserCardProps {
  user: User;
}

export function UserCard({ user }: UserCardProps) {
  return (
    <div className="user-card">
      <Avatar src={user.avatar} alt={user.name} />
      <div>
        <h3>{user.name}</h3>
        <p>{user.email}</p>
      </div>
    </div>
  );
}

// src/entities/user/index.ts
export { UserCard } from './ui/UserCard';
export { getUser, getCurrentUser } from './api/getUser';
export type { User } from './model/types';
shared レイヤー

目的: 再利用可能なユーティリティ、UI コンポーネント、サードパーティ連携を扱います。

役割:

  • UI キット(ボタン、入力欄、カードの各コンポーネント)
  • ヘルパー関数(フォーマッター、バリデーター)
  • API クライアントの設定
  • 定数と環境変数
  • サードパーティライブラリとの連携

インポートルール: FSD のどのレイヤーからもインポートできません(外部パッケージのみ)。

スライスなし: セグメントを直接配置します(ui/、lib/、api/、config/)。

例:

// src/shared/ui/button/Button.tsx
import { type ButtonHTMLAttributes } from 'react';

interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary' | 'ghost';
  size?: 'sm' | 'md' | 'lg';
}

export function Button({
  variant = 'primary',
  size = 'md',
  className,
  children,
  ...props
}: ButtonProps) {
  return (
    <button
      className={`button button--${variant} button--${size} ${className}`}
      {...props}
    >
      {children}
    </button>
  );
}

// src/shared/lib/format.ts
export function formatDate(date: Date): string {
  return new Intl.DateTimeFormat('en-US').format(date);
}

export function formatCurrency(amount: number): string {
  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: 'USD',
  }).format(amount);
}

// src/shared/api/client.ts
import axios from 'axios';

export const apiClient = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_URL,
  headers: {
    'Content-Type': 'application/json',
  },
});

// src/shared/config/env.ts
export const env = {
  apiUrl: process.env.NEXT_PUBLIC_API_URL!,
  nodeEnv: process.env.NODE_ENV,
} as const;

ワークフロー

ステップ 1:レイヤーディレクトリを設定する

FSD のフォルダー構成を作成します:

mkdir -p src/{app,views,widgets,features,entities,shared}
mkdir -p src/app/{providers,styles,config}
mkdir -p src/shared/{ui,lib,api,config}
ステップ 2:最初のエンティティを作成する

最下層のエンティティから始めます。中核となるビジネスモデルを定義します。

// src/entities/user/model/types.ts
export interface User {
  id: string;
  name: string;
  email: string;
}

// src/entities/user/api/getUser.ts
export async function getUser(id: string): Promise<User> {
  // API implementation
}

// src/entities/user/index.ts
export type { User } from './model/types';
export { getUser } from './api/getUser';
ステップ 3:エンティティを使ってフィーチャーを構築する

エンティティを使うフィーチャーを作成します。

// src/features/user-profile/ui/UserProfile.tsx
import { User } from '@/entities/user'; // ✅ Feature imports entity

export function UserProfile({ user }: { user: User }) {
  return <div>{user.name}</div>;
}

// src/features/user-profile/index.ts
export { UserProfile } from './ui/UserProfile';
ステップ 4:フィーチャーからウィジェットを構成する

複合ウィジェットを構築します。

// src/widgets/header/ui/Header.tsx
import { UserProfile } from '@/features/user-profile'; // ✅ Widget imports feature
import { SearchBar } from '@/features/search';

export function Header({ user }) {
  return (
    <header>
      <SearchBar />
      <UserProfile user={user} />
    </header>
  );
}
ステップ 5:ビューを組み立てる

ページレベルのビューを作成します。

// src/views/dashboard/ui/DashboardView.tsx
import { Header } from '@/widgets/header'; // ✅ View imports widget

export function DashboardView() {
  return (
    <div>
      <Header />
      {/* More content */}
    </div>
  );
}

// src/views/dashboard/index.ts
export { DashboardView } from './ui/DashboardView';
ステップ 6:アプリルーターに接続する

ビューを Next.js のルーティングに接続します。

// app/dashboard/page.tsx
import { DashboardView } from '@/views/dashboard';

export default function DashboardPage() {
  return <DashboardView />;
}

インポートルールと依存関係

許可されるインポートパターン
// ✅ Layer importing from layer below
import { User } from '@/entities/user';          // Feature → Entity
import { LoginForm } from '@/features/auth';     // Widget → Feature
import { Header } from '@/widgets/header';       // View → Widget

// ✅ Any layer importing from shared
import { Button } from '@/shared/ui/button';
import { formatDate } from '@/shared/lib/format';

// ✅ Slice importing from different slice in lower layer
import { User } from '@/entities/user';          // features/auth → entities/user
import { Post } from '@/entities/post';          // features/like → entities/post
禁止されるインポートパターン
// ❌ Layer importing from same or higher layer
import { DashboardView } from '@/views/dashboard';  // Feature → View (upward)
import { Header } from '@/widgets/header';          // Feature → Widget (upward)
import { LoginForm } from '@/features/login';       // features/auth → features/login (same layer)

// ❌ Cross-slice imports within same layer
import { SearchBar } from '@/features/search';      // features/auth → features/search

// ❌ Shared importing from FSD layers
import { User } from '@/entities/user';             // shared/lib → entities/user
有効な例と無効な例

無効(フィーチャー間のインポート):

// ❌ src/features/search/ui/SearchBar.tsx
import { LoginForm } from '@/features/auth'; // Same layer import

有効(ウィジェットに切り出す):

// ✅ src/widgets/navbar/ui/Navbar.tsx
import { SearchBar } from '@/features/search';
import { LoginForm } from '@/features/auth';

export function Navbar() {
  return (
    <nav>
      <SearchBar />
      <LoginForm />
    </nav>
  );
}
循環依存の解消

問題:

// features/auth imports features/user-settings
// features/user-settings imports features/auth
// ❌ Circular dependency

解決策 1:エンティティに切り出す

// Move shared logic to entities/user
// Both features import from entities/user
// ✅ No circular dependency

解決策 2:ウィジェットに切り出す

// Create widgets/user-panel that imports both features
// ✅ Widget layer can import from features
パブリック API の適用

エクスポートを制御するには、常に index.ts を使用します。

// src/features/auth/index.ts
export { LoginForm } from './ui/LoginForm';
export { useAuth } from './model/useAuth';
export type { AuthState } from './model/types';

// ❌ Do NOT export internal helpers
// export { validatePassword } from './lib/validation'; // Keep internal

パブリック API からのみインポートします。

// ✅ Correct
import { LoginForm } from '@/features/auth';

// ❌ Wrong (deep import)
import { LoginForm } from '@/features/auth/ui/LoginForm';

セグメントのパターン

ui/ セグメント

目的: React コンポーネントと視覚要素。

使用する場面:

  • あらゆる React コンポーネント
  • UI の構成
  • 視覚的な表示

例:

// src/entities/user/ui/UserCard.tsx
import type { User } from '../model/types';

export function UserCard({ user }: { user: User }) {
  return (
    <div className="user-card">
      <h3>{user.name}</h3>
      <p>{user.email}</p>
    </div>
  );
}
model/ セグメント

目的: ビジネスロジック、状態管理、型定義。

使用する場面:

  • TypeScript のインターフェースと型
  • 状態管理用の React フック
  • ビジネスロジック関数
  • データ変換

例:

// src/features/auth/model/useAuth.ts
'use client';

import { create } from 'zustand';
import type { User } from '@/entities/user';

interface AuthState {
  user: User | null;
  isAuthenticated: boolean;
  login: (user: User) => void;
  logout: () => void;
}

export const useAuth = create<AuthState>((set) => ({
  user: null,
  isAuthenticated: false,
  login: (user) => set({ user, isAuthenticated: true }),
  logout: () => set({ user: null, isAuthenticated: false }),
}));
api/ セグメント

目的: API クライアント、データ取得、外部連携。

使用する場面:

  • HTTP リクエスト
  • WebSocket 接続
  • サードパーティ API との連携
  • サーバーアクション(Next.js)

例:

// src/entities/user/api/userApi.ts
'use server';

import { apiClient } from '@/shared/api/client';
import type { User } from '../model/types';

export async function fetchUsers(): Promise<User[]> {
  const response = await apiClient.get('/users');
  return response.data;
}

export async function createUser(data: Omit<User, 'id'>): Promise<User> {
  const response = await apiClient.post('/users', data);
  return response.data;
}
lib/ セグメント

目的: スライス固有のユーティリティ関数とヘルパー。

使用する場面:

  • スライス固有のユーティリティ
  • 検証関数
  • データ変換ヘルパー

例:

// src/features/auth/lib/validation.ts
import { z } from 'zod';

export const loginSchema = z.object({
  email: z.string().email('Invalid email address'),
  password: z.string().min(8, 'Password must be at least 8 characters'),
});

export function validateLogin(data: unknown) {
  return loginSchema.parse(data);
}
config/ セグメント

目的: 設定定数とフィーチャーフラグ。

使用する場面:

  • フィーチャー固有の定数
  • 設定オブジェクト
  • フィーチャーフラグ

例:

// src/app/config/theme.ts
export const theme = {
  colors: {
    primary: '#0070f3',
    secondary: '#ff4081',
  },
  breakpoints: {
    sm: '640px',
    md: '768px',
    lg: '1024px',
  },
} as const;

移行戦略

既存の Next.js アプリを FSD に移行する

ボトムアップ方式(推奨):

  1. shared レイヤーから始める

    • 共通の UI コンポーネントを shared/ui/ に切り出す
    • ユーティリティを shared/lib/ に移動する
    • shared/api/ で API クライアントを設定する
  2. エンティティを定義する

    • ビジネスドメインのオブジェクト(User、Post、Comment)を特定する
    • entities/{name}/model/ にエンティティの型を作成する
    • CRUD 操作を entities/{name}/api/ に移動する
  3. フィーチャーを切り出す

    • ユーザー操作(ログイン、検索、いいね)を特定する
    • features/{name}/ にフィーチャーのスライスを作成する
    • フィーチャー内でエンティティを使用する
  4. ウィジェットを構築する

    • 複合 UI ブロック(ヘッダー、サイドバー)を特定する
    • widgets/{name}/ にウィジェットのスライスを作成する
    • ウィジェット内でフィーチャーを構成する
  5. ビューを整理する

    • ページのロジックを /app から /src/views に移動する
    • ルーティングは /app に、ビジネスロジックは /src/views に置く
    • ビュー内でウィジェットを構成する
  6. アプリレイヤーを設定する

    • グローバルプロバイダーを app/providers/ に移動する
    • グローバルスタイルを app/styles/ に移動する
既存コードへの対応

段階的な移行:

  • 一度に 1 ページずつ移行する
  • 最も複雑でないページから始める
  • 移行中は古い構成と新しい構成を併用する
  • 移行に合わせてインポートを更新する

全工程を通じたテスト:

  • 各レイヤーの移行後にテストを実行する
  • 機能に不具合がないことを確認する
  • インポートパスが正しいことを確認する

ベストプラクティス

スライスを分離して保ちます:

  • 同じレイヤー内のスライス間でインポートしないでください
  • 各スライスは独立させます
  • 共通ロジックは下位レイヤーに抽出します

パブリック API パターンを使用します:

  • 常に index.ts を通じてエクスポートします
  • 深い階層からのインポートを防ぎます
  • リファクタリングが容易になります

テストを同じ場所に配置します:

features/
└── auth/
    ├── ui/
    │   ├── LoginForm.tsx
    │   └── LoginForm.test.tsx  # Test next to implementation
    └── index.ts

「何でも詰め込んだスライス」を避けます:

  • スライスは単一の責務に絞ります
  • 大きなスライスは複数の小さなスライスに分割します
  • 共通ロジックは共有レイヤーに抽出します

技術ではなくドメインに基づいて命名します:

  • ✅ features/product-search
  • ❌ features/search-bar-component

TypeScript の strict モードを使用します:

{
  "compilerOptions": {
    "strict": true
  }
}

アーキテクチャ上の決定を文書化します:

  • ADR(アーキテクチャ決定記録)を維持します
  • 特定のスライスが存在する理由を記録します
  • レイヤー境界の決定について説明します

よくあるパターン

共有 UI コンポーネント
// src/shared/ui/button/Button.tsx
export function Button({ children, ...props }) {
  return <button {...props}>{children}</button>;
}

// Usage in feature
import { Button } from '@/shared/ui/button';
API クライアントの設定
// src/shared/api/client.ts
import axios from 'axios';

export const apiClient = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_URL,
});

// Usage in entity
import { apiClient } from '@/shared/api/client';
フィーチャーを使ったフォーム処理
// src/features/product-form/ui/ProductForm.tsx
'use client';

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { productSchema } from '../lib/validation';
import { createProduct } from '../api/createProduct';

export function ProductForm() {
  const { register, handleSubmit } = useForm({
    resolver: zodResolver(productSchema),
  });

  return <form onSubmit={handleSubmit(createProduct)}>...</form>;
}
認証の統合
// src/features/auth/model/useAuth.ts
export const useAuth = create<AuthState>((set) => ({...}));

// src/widgets/header/ui/Header.tsx
import { useAuth } from '@/features/auth';

export function Header() {
  const { user } = useAuth();
  return <header>Welcome, {user?.name}</header>;
}
サーバーコンポーネントを使ったデータ取得
// app/dashboard/page.tsx
import { DashboardView } from '@/views/dashboard';
import { getUser } from '@/entities/user';

export default async function DashboardPage() {
  const user = await getUser('current');
  return <DashboardView user={user} />;
}

// src/views/dashboard/ui/DashboardView.tsx
import type { User } from '@/entities/user';

export function DashboardView({ user }: { user: User }) {
  return <div>Welcome, {user.name}</div>;
}

トラブルシューティング

循環依存

問題: 2 つのスライスが互いをインポートしています。

解決策:

  1. 共通ロジックを下位レイヤー(通常はエンティティまたは共有レイヤー)に抽出します
  2. 両方をインポートする上位レイヤー(ウィジェット)を作成します
  3. いずれかのスライスを複数のスライスに分割すべきか検討します
インポートパスの問題

問題: TypeScript が @/ インポートを解決できません。

解決策: tsconfig.json でパスエイリアスを設定します:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/app/*": ["src/app/*"],
      "@/views/*": ["src/views/*"],
      "@/widgets/*": ["src/widgets/*"],
      "@/features/*": ["src/features/*"],
      "@/entities/*": ["src/entities/*"],
      "@/shared/*": ["src/shared/*"]
    }
  }
}
ビルドエラー

問題: 再構成後、Next.js がモジュールを見つけられません。

解決策:

  1. .next ディレクトリをクリアします:rm -rf .next
  2. 依存関係を再インストールします:npm install
  3. 開発サーバーを再起動します:npm run dev

設定

TypeScript のパスエイリアス
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/app/*": ["src/app/*"],
      "@/views/*": ["src/views/*"],
      "@/widgets/*": ["src/widgets/*"],
      "@/features/*": ["src/features/*"],
      "@/entities/*": ["src/entities/*"],
      "@/shared/*": ["src/shared/*"]
    }
  },
  "include": ["src", "app"]
}
ESLint のインポートルール(任意)
// .eslintrc.js
module.exports = {
  rules: {
    'no-restricted-imports': [
      'error',
      {
        patterns: [
          {
            group: ['@/views/*', '@/widgets/*'],
            message: 'Features cannot import from views or widgets',
          },
        ],
      },
    ],
  },
};

参考資料

コメント