はじめに
アプリケーション開発において、ディレクトリ構成は保守性・拡張性・開発効率に直結する設計要素です。
本記事では、以下のような課題に悩む現場に向けて、「機能ごとに整理しやすく、拡張にも強い」フィーチャーベース構成をご紹介します。
- プロジェクトが大きくなるとファイルが散乱し、管理しにくくなる
- 新規メンバーが「どこに何があるか」迷いやすい
- 小さく始めた構成が、成長とともに破綻しやすい
⚠️ この構成が「唯一の正解」ではありません。
本記事は、筆者が実案件・チーム開発の中で得た知見をもとに「比較的扱いやすく、現実的に運用しやすい」と感じた構成パターンの一例です。
チームの人数・技術レベル・プロジェクトの性質に応じて、適宜カスタマイズして取り入れてください。
構成方針
- 機能(feature)単位でディレクトリを分ける
- 各機能内はファイル単位で責務を整理(
components.tsx
,usecases.ts
など) - 複数の feature を組み合わせる処理(画面UI/レスポンス)は外側で組む
- 共通処理は
shared/
、アプリ基盤はcore/
に配置
features の分け方の基準
「何を1つの feature と見なすか」は、構成全体の見通しと保守性に大きく影響します。以下のような基準を参考に設計しましょう。
分類基準 | 内容 |
---|---|
業務ドメイン単位 | 例:user , product , order , dashboard |
画面単位 | 責務が明確な単一画面(例:Login , Profile , Settings ) |
API単位 | REST/GraphQL のエンドポイント構成に準拠 |
変更頻度・チーム単位 | 頻繁に改修が入る機能や、担当チームごとに分ける場合も有効 |
「何を1つの feature とするか」は、構成全体の可読性と拡張性を左右します。以下のような視点で切り分けるのがおすすめです。
💡 基本は「1つの機能で完結する画面・ロジック・状態」をひとまとめに。
境界が曖昧な場合は最初は大まかに分け、必要に応じて分割・統合していくのが現実的です。
具体例
login
,signup
,forgotPassword
は最初はauth/
にまとめる
→ 認証系の UI・API・状態管理を集約して管理しやすくなるuser
,profile
,settings
などユーザー周辺機能は
→ 当初はuser/
配下に一緒に置いても良い
→ 将来的にprofile/
やsettings/
を独立して切り出してもOK
ディレクトリ構成例
フロントエンド
src/
├── features/ // 機能単位でまとめる(UI・ロジック・状態)
│ ├── user/
│ │ ├── components.tsx // ユーザー機能専用の UI コンポーネント
│ │ ├── hooks.ts // カスタムフック
│ │ ├── usecases.ts // ユースケース(UIから呼び出す操作単位)
│ │ ├── repository.ts // データ取得処理(API, localStorageなど)
│ │ └── store.ts // Zustand, Redux などの状態管理
│ ├── product/
│ └── order/
│
├── pages/ // 複数 feature を組み合わせて画面を構築
│ ├── dashboard.tsx
│ └── user.tsx
│
├── shared/ // 再利用可能な UI / ユーティリティ群
│ ├── components.tsx // 共通UI(Button, Modalなど)
│ ├── utils.ts // 汎用関数(純関数)
│ ├── constants.ts // 定数
│ └── types.ts // 共通型
│
├── core/ // アプリ全体の基盤機能
│ ├── api.ts // API クライアント設定(axios等)
│ ├── auth.ts // 認証関連処理
│ ├── config.ts // 環境設定
│ └── store.ts // グローバルストアの初期化
│
└── main.tsx // エントリーポイント
- 各 feature の
components.tsx
に、機能固有の UI を定義 - 状態管理・API ロジックは
store.ts
/hooks.ts
/usecases.ts
に記述 - それらを
pages/
で自由に組み合わせて画面を構成
💡設計のポイント
features/*
では 状態・ロジック・UI を完結させる- 複数機能の組み合わせて
pages/
にまとめて 画面UI を構築 - 共通部品・定数・型は
shared/
に切り出し DRY を意識 - アプリ全体の設定や状態管理は
core/
に集約
UI の組み立て:(単一 feature のみで構成される例)
// pages/user.tsx
import { UserList } from '@/features/user/components';
import { useUser } from '@/features/user/hooks';
export const UserPage = () => {
const { users } = useUser();
return <UserList users={users} />;
};
UI の組み立て(複数 feature で構成される例)
// pages/dashboard.tsx
import { UserCard } from '@/features/user/components';
import { ProductCard } from '@/features/product/components';
import { useUser } from '@/features/user/hooks';
import { useProduct } from '@/features/product/hooks';
export const Dashboard = () => {
const { currentUser } = useUser();
const { featuredProduct } = useProduct();
return (
<>
<UserCard name={currentUser.name} />
<ProductCard product={featuredProduct} />
</>
);
};
バックエンド
src/
├── features/ // 各機能のドメインロジック
│ ├── user/
│ │ ├── usecases.ts // ユースケース(アプリケーションサービス層)
│ │ ├── repository.ts // DBや外部APIアクセス
│ │ └── types.ts // 型定義
│ ├── product/
│ └── order/
│
├── routes/ // エンドポイント単位のルーティング(レスポンス構成)
│ ├── user/route.ts // /api/users
│ └── dashboard/route.ts // /api/dashboard(複数featureの合成)
│
├── shared/ // 共通型・定数・関数
│ ├── utils.ts
│ ├── constants.ts
│ └── types.ts
│
├── core/ // アプリ全体の初期化・共通処理
│ ├── config.ts
│ ├── database.ts
│ ├── auth.ts
│ └── errorHandler.ts
│
└── main.ts // エントリーポイント
💡設計のポイント
features/*
は ロジックとデータ取得に専念(サービス・DB)- エンドポイント処理は
routes/*/route.ts
に集約 - レスポンス構造は自由に構成できるよう、戻り値の組み合わせで柔軟に設計
- すべてのルートは
main.ts
で明示的にマウントし、アプリの起点を一元管理
APIの組み立て(単一 feature のみで構成される例)
// routes/user/route.ts
import { Router } from 'express';
import { getUserById } from '@/features/user/usecase';
const router = Router();
router.get('/:userId', async (req, res) => {
const user = await getUserById(req.params.userId);
res.json(user);
});
export default router;
APIの組み立て(複数 feature のみで構成される例)
// routes/dashboard/route.ts
import { Router } from 'express';
import { getUserById } from '@/features/user/usecase';
import { getOrdersByUserId } from '@/features/order/usecase';
import { getProductsByIds } from '@/features/product/usecase';
const router = Router();
router.get('/:userId', async (req, res) => {
const userId = req.params.userId;
const user = await getUserById(userId);
const orders = await getOrdersByUserId(userId);
const productIds = orders.flatMap((o) => o.productIds);
const products = await getProductsByIds(productIds);
res.json({
user: {
name: user.name,
email: user.email,
},
orderSummary: {
count: orders.length,
products,
},
});
});
export default router;
ルーティングの結合例
// main.ts
import express from 'express';
import dashboardRoute from './routes/dashboard/route';
import userRoute from './routes/user/route';
const app = express();
app.use(express.json());
app.use('/api/dashboard', dashboardRoute);
app.use('/api/users', userRoute);
app.listen(3000, () => {
console.log('Server is running on <http://localhost:3000>');
});
おわりに
この構成は、以下のような開発スタイルにフィットします:
- 複数人・複数機能で並行して開発するチーム
- 小さく始めて段階的に機能追加していきたいプロジェクト
- 機能ごとに責務を分離し、変更しやすく保守しやすい設計を求める現場
構成は一度決めたら終わりではなく、育てていくもの。
現在の最適解が未来のボトルネックにならないよう、シンプルな原則を軸に柔軟な見直しを続けていきましょう。