FDD-architecture について
React + Viteプロジェクト向けに、ビジネス機能単位でコードを整理するFDDアーキテクチャスキルです。コンポーネント、フック、型、テストを機能モジュールにまとめ、境界やインポート、命名、共有コードの指針を示します。
主な機能
- 01ビジネス機能単位のモジュール構成
- 02index.tsによる公開APIの定義
- 03機能内外のインポート規則
- 04ファイルとディレクトリの命名規則
- 05共有コードへの昇格指針
活用シーン
- 01React + Viteプロジェクトの構成設計
- 02機能モジュールの作成や再構成
- 03機能間インポートとAPI境界の確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ jenishshrestha/ ai-skills --skill fdd-architectureインストール後、Claude Code で/fdd-architectureを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
metadata.jsonevals/evals.jsonreferences/migration.mdrules/named-imports.mdnaming-consistency.mdlocality-co-location.mdlocality-depth.mdintra-feature-imports.mdhook-extraction.mdapi-boundary.mdshared-component-organization.mdshared-global-move.md詳細
React + Vite 向けの機能駆動アーキテクチャ(FDD)
技術的な種類ではなく、ビジネス機能ごとにコードを整理します。各機能は、独自のコンポーネント、フック、型、テストを持つ自己完結型のモジュールです。
プロジェクト構成
src/
├── features/ # Feature modules (business capabilities)
│ ├── auth/
│ │ ├── components/
│ │ ├── hooks/
│ │ ├── lib/
│ │ ├── types/
│ │ ├── index.ts # Public API
│ │ └── auth.test.tsx
│ └── dashboard/
│ └── ...
├── shared/ # Shared infrastructure (3+ feature rule)
│ ├── components/
│ │ ├── ui/ # Primitive components (Button, Input)
│ │ ├── layouts/ # Layout wrappers (Header, Sidebar)
│ │ └── providers/ # Global providers (ThemeProvider)
│ ├── hooks/
│ ├── lib/
│ └── types/
└── app/ # App entry and routing
├── routes/
└── main.tsx
React の品質原則
これらはアーキテクチャだけでなく、このプロジェクトで記述するすべてのコードに適用されます。
KISS — シンプルに保つ
気の利いた抽象化よりも、標準的な React のパターンを優先します。コメントなしではロジックを理解しにくい場合は、シンプルにしてください。useState で十分なときに、複雑な状態管理に頼らないでください。
YAGNI — 今必要なものだけを作る
「念のため」の props、推測に基づく抽象化、時期尚早な最適化を追加しないでください。今必要なものを作りましょう。現時点で利用する箇所がない prop は削除してください。
設定よりコンポジション
コンポーネントに 10 個以上のブール値の props が蓄積してきたら、複合コンポーネントパターンを使って、組み合わせ可能なパーツに分割するサインです。
// ❌ Configuration overload
<Card showHeader showFooter isCollapsible hasBorder variant="outlined" />
// ✅ Composition
<Card>
<Card.Header>Title</Card.Header>
<Card.Content>Body</Card.Content>
<Card.Footer>Actions</Card.Footer>
</Card>
明示的な予測可能性
useEffect に隠れた副作用を持たせないでください。明示的なイベントハンドラーと安定したコールバック参照を優先します。何かが起きたとき、読み手が effect の依存配列を探し回らなくても、その理由をたどれるようにしてください。
// ❌ Hidden side-effect — runs on every query change
useEffect(() => {
trackAnalytics("search", { query });
}, [query]);
// ✅ Explicit — fires on user action
const handleSearch = (query: string) => {
setQuery(query);
trackAnalytics("search", { query });
};
アクセシビリティ(a11y)
すべてのインタラクティブ要素で、キーボード操作への対応、適切な ARIA 属性、視認できるフォーカスインジケーターが必要です。role 属性を持つ汎用的な div よりも、セマンティックな HTML 要素(button、nav、main)を使用してください。
FDD のルール — クイックリファレンス
1. 機能の局所性(最重要)
機能のコードはすべて src/features/[feature-name]/ の中に置きます。ネストは最大 3 階層に制限してください。
src/features/user-profile/
├── components/
│ ├── profile-card.tsx
│ └── avatar-upload.tsx
├── hooks/
│ └── use-profile.ts
├── lib/
│ └── format-name.ts
├── types/
│ └── profile.ts
├── index.ts
└── user-profile.test.tsx
2. 公開 API の境界(重要)
各機能は index.ts を通じてエクスポートします。機能の内部実装から直接インポートしないでください。
// src/features/auth/index.ts
export { LoginForm } from "./components/login-form";
export { useAuth } from "./hooks/use-auth";
export type { AuthUser, AuthState } from "./types";
// ❌ Importing from internals
import { LoginButton } from "@/features/auth/components/login-button";
// ✅ Importing from public API
import { LoginButton } from "@/features/auth";
3. インポート規約(重要)
機能内 — 相対パスを使用します。
import { useAuth } from "../hooks/use-auth";
機能間 — 公開 API を介したエイリアスを使用します。
import { useAuth } from "@/features/auth";
型 — 常に import type を使用します。
import type { User } from "./types";
4. 命名規約(中程度)
| 種類 | 規約 | 例 |
|---|---|---|
コンポーネントファイル(.tsx) | PascalCase | ProductCard.tsx、DataTable.tsx |
フックファイル(.ts) | use を接頭辞にした camelCase | useDataTable.ts、useUserProfile.ts |
| 型ファイル | kebab-case + .types.ts | product.types.ts、user-profile.types.ts |
| スキーマファイル | kebab-case + .schema.ts | auth.schema.ts、product.schema.ts |
| フォルダー | kebab-case | user-profile/、data-table/ |
| ユーティリティ / lib / api / config | kebab-case | fetch-users.ts、utils.ts |
名前付きインポートのみを使用し、import * は使用しないでください。詳細と例外については、rules/naming-consistency.md を参照してください。
5. 共有インフラストラクチャ(中程度)
三回ルール — src/shared/ に移すのは、3 つ以上の機能で使われるようになってからにします。
- 1 つの機能 → 機能フォルダー内に置く
- 2 つの機能 → 元の場所に置くか、ロジックが異なる方向に進化しそうな場合は複製する
- 3 つ以上の機能 →
src/shared/に移す
6. フックの切り出し
次の場合は、ビジネスロジックをコンポーネントから切り出します。
- コンポーネントに、レンダリング以外のロジックが 5 行以上ある
- ロジックを機能内で再利用できる
- ロジックを単独でテストする必要がある
// ❌ Logic mixed with UI
function ProfileCard() {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/user').then(res => res.json()).then(setUser);
}, []);
return <div>{user?.name}</div>;
}
// ✅ Logic extracted to hook
function ProfileCard() {
const { user, loading } = useUserProfile();
return <div>{user?.name}</div>;
}
詳細ルール
詳しい説明、例、エッジケースについては、rules/ 内の該当するルールファイルをお読みください。必要なものだけを読み込んでください。
| ルールファイル | 読み込むタイミング |
|---|---|
locality-co-location.md | 新しい機能を作成するとき、ファイルの配置場所を決めるとき |
locality-depth.md | 機能フォルダーの階層が深くなり、再構成が必要なとき |
api-boundary.md | index.ts のエクスポートを設定するとき、機能間のインポートを確認するとき |
intra-feature-imports.md | 相対インポートと絶対インポートのどちらを使うか決めるとき |
naming-consistency.md | 新しいファイルやフォルダーに名前を付けるとき |
named-imports.md | インポート形式に関する質問、バレルファイルの設定 |
shared-global-move.md | コードを共有領域に移すべきか決めるとき |
shared-component-organization.md | src/shared/components/ のサブディレクトリーを整理するとき |
hook-extraction.md | 複雑なコンポーネントからロジックを抽出するとき |
ツール
Vite のパスエイリアス(vite.config.ts)
export default defineConfig({
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
"@/features": path.resolve(__dirname, "./src/features"),
"@/shared": path.resolve(__dirname, "./src/shared"),
},
},
});
TypeScript のパス(tsconfig.json)
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@/features/*": ["./src/features/*"],
"@/shared/*": ["./src/shared/*"]
}
}
}
ESLint — 公開 API の境界を強制
{
"rules": {
"no-restricted-imports": [
"error",
{
"patterns": [
{
"group": ["@/features/*/components/*", "@/features/*/hooks/*"],
"message": "Import from feature's public API (index.ts) only"
}
]
}
]
}
}
機能ベースのコード分割(TanStack Router)
const dashboardRoute = createRoute({
path: "/dashboard",
component: () => import("@/features/dashboard").then((m) => m.DashboardPage),
});
移行ガイド(フラットな構成 → FDD、Next.js → Vite)については、references/migration.md をご覧ください。