はじめに
アプリケーション開発において、ディレクトリ構成は保守性・拡張性・開発効率に直結する設計要素です。
本記事では、以下のような課題に悩む現場に向けて、「シンプルで直感的、責務ごとの分離が容易」であるレイヤーベース構成をご紹介します
⚠️ この構成が「唯一の正解」ではありません。
本記事は、筆者が実案件・チーム開発の中で得た知見をもとに「比較的扱いやすく、現実的に運用しやすい」と感じた構成パターンの一例です。
チームの人数・技術レベル・プロジェクトの性質に応じて、適宜カスタマイズして取り入れてください。
構成方針
レイヤーベースの構成は、アプリケーションの機能を役割ごと(レイヤー)に分割する方法です。MVCやクリーンアーキテクチャのような概念に基づいて整理されるため、直感的に理解しやすいのが特徴です。
- 役割(レイヤー)ごとにディレクトリを分ける
- 各レイヤーは責務を明確に分離(
controllers/
,services/
,models/
など) - ビジネス処理の流れを階層的に構築することで、見通しの良さと変更のしやすさを担保
メリット
- シンプルで直感的 各レイヤーが明確に分離されているため、新規参画者が学習しやすい。
- コードの責務ごとの分離が容易 モジュール間の依存関係が整理されており、変更の影響範囲を限定しやすい。
- 小〜中規模プロジェクトに適している シンプルなプロジェクトでは、レイヤーベースの構成が管理しやすい。
デメリット
- 大規模プロジェクトではフォルダ構成が肥大化しやすい 機能が増えると、各レイヤーの管理が煩雑になる。
- 機能ごとの依存関係が複雑になる場合がある レイヤーをまたぐ処理が増えると、変更時の影響範囲が広がる。
レイヤーの分け方の基準
「どこまでをレイヤーとして分離するか」は、構成全体の保守性やチーム内の役割分担に大きく影響します。以下のような基準を参考に、システムの性質や開発体制に合わせて柔軟に設計しましょう。
分類基準 | 内容 |
---|---|
責務の明確さ | 表示、状態操作、ビジネスロジック、データアクセスなど、それぞれの関心ごとを分離 |
変更頻度の違い | UI変更が頻繁なcomponents/ と、ロジック中心で安定しやすいusecases/ などを分けることで、影響範囲を限定 |
再利用性の高さ | 共通化しやすい処理(hooks、utils、middlewareなど)をレイヤーとして独立させる |
テスト単位 | 単体テストしやすい粒度(usecase, repositoryなど)で切り出す |
開発体制・スキル分離 | UI中心の開発と、ドメインロジック中心の開発を分業する際にも有効 |
💡 基本は「画面・ロジック・データ取得が階層的に分離される構成」を目指します。
明確な境界が引きづらい部分は、まずは統合しておき、規模や責務が増した段階で段階的に分割するのが現実的です。
例えば、はじめは pages/user.tsx
に画面・ロジック・データ取得に関するコードを書いておき、その後に components/UserList.tsx
を作成してユーザー一覧のUIを分離し、データ取得系の処理を repository/userRepository.ts
を作成してそこに記述するなど
ディレクトリ構成例
フロントエンド
src/
├── core/ // アプリ全体の基盤
├── pages/ // ページ単位の構成(画面エントリ)
│ ├── dashboard.tsx
│ └── user.tsx
├── components/ // UI部品(usecaseやhooksを利用)
│ ├── UserList.tsx
│ └── DashboardCard.tsx
├── hooks/ // 状態操作・副作用抽象化
│ ├── useUser.ts
│ └── useProduct.ts
├── usecases/ // アプリケーションロジック
│ ├── userUsecase.ts
│ └── productUsecase.ts
├── repository/ // データ取得処理(APIアクセスなど)
│ ├── userRepository.ts
│ └── productRepository.ts
├── store/ // ZustandやReduxなどの状態管理
│ └── userStore.ts
├── utils/ // 汎用関数
│ └── format.ts
└── tsconfig.json
- 各 UI コンポーネントは
components/
に配置し、表示専用の責務を担う - 状態操作や副作用ロジックは
hooks/
に分離し、UI から切り離して再利用性を高める - アプリケーションの操作単位(ユースケース)は
usecases/
に集約し、画面ごとの組み合わせはpages/
で行う
💡設計のポイント
- レイヤーごとに単一責務を明確に分離することで、各層の変更・テストがしやすくなる
pages/
では、hooks/usecases/components を自由に組み合わせて UI を構成する- 状態管理やデータ取得処理は usecase 経由でまとめて呼び出すようにすると、疎結合を保ちやすい
core/
は アプリ全体の初期化や設定、APIクライアントの共通処理を担う
UIの組み立て例
// pages/user.tsx
import { UserList } from '@/components/UserList';
import { useUser } from '@/hooks/useUser';
export const UserPage = () => {
const { users } = useUser();
return <UserList users={users} />;
};
// pages/dashboard.tsx
import { UserCard } from '@/components/UserCard';
import { ProductCard } from '@/components/ProductCard';
import { useUser } from '@/hooks/useUser';
import { useProduct } from '@/hooks/useProduct';
export const Dashboard = () => {
const { currentUser } = useUser();
const { featuredProduct } = useProduct();
return (
<>
<UserCard name={currentUser.name} />
<ProductCard product={featuredProduct} />
</>
);
};
バックエンド
src/
├── routers/ // エンドポイント単位でルーティング定義
│ ├── user/route.ts
│ └── dashboard/route.ts
├── usecases/ // アプリケーションロジック
│ ├── userUsecase.ts
│ ├── orderUsecase.ts
│ └── productUsecase.ts
├── repository/ // DBや外部APIとの接続
│ ├── userRepository.ts
│ ├── orderRepository.ts
│ └── productRepository.ts
├── middleware/ // 共通処理(例:auth, errorHandler)
│ └── errorHandler.ts
├── types/ // 共通型定義
│ └── user.ts
├── utils/ // 共通関数
│ └── logger.ts
└── tsconfig.json
- 各エンドポイントは
routers/
に配置し、HTTPリクエスト/レスポンスの処理を担当 - アプリケーションの操作単位(ユースケース)は
usecases/
に実装し、ルータから呼び出す構造 - 永続化処理や外部APIとのやり取りは
repository/
に集約して、ドメインロジックと分離 - 共通のエラーハンドリングや認証は
middleware/
で定義し、関心の分離と再利用性を担保
💡設計のポイント
- 各レイヤーは明確な責務と依存方向(上→下)を持つように構成する
- エンドポイント処理は
routes/*/route.ts
に集約 - レスポンス構造は自由に構成できるよう、戻り値の組み合わせで柔軟に設計
- すべてのルートは
main.ts
で明示的にマウントし、アプリの起点を一元管理
APIの組み立て例
// routers/user/route.ts
import { Router } from 'express';
import { getUserById } from '@/usecases/userUsecase';
const router = Router();
router.get('/:userId', async (req, res) => {
const user = await getUserById(req.params.userId);
res.json(user);
});
export default router;
// routers/dashboard/route.ts
import { Router } from 'express';
import { getUserById } from '@/usecases/userUsecase';
import { getOrdersByUserId } from '@/usecases/orderUsecase';
import { getProductsByIds } from '@/usecases/productUsecase';
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>');
});
おわりに
この構成は、以下のような開発スタイルにフィットします
- 少人数チームや経験の浅いメンバーを含む開発体制
- ドメインが複雑で、ロジックの見通しや整理が重要なプロジェクト
- 初期は小さく始め、段階的に機能を追加していくアプローチ
構成は一度決めたら終わりではなく、育てていくもの。
現在の最適解が未来のボトルネックにならないよう、シンプルな原則を軸に柔軟な見直しを続けていきましょう。
また、本ディレクトリ構成例は、あくまで一例になりますので、言語やフレームワークに適した構成に適宜変換してください。