ディレクトリ構成 ~レイヤーベース編~

Kodai Nakahara

2025.9.26

はじめに

アプリケーション開発において、ディレクトリ構成は保守性・拡張性・開発効率に直結する設計要素です。

本記事では、以下のような課題に悩む現場に向けて、「シンプルで直感的、責務ごとの分離が容易」であるレイヤーベース構成をご紹介します

⚠️ この構成が「唯一の正解」ではありません。
本記事は、筆者が実案件・チーム開発の中で得た知見をもとに「比較的扱いやすく、現実的に運用しやすい」と感じた構成パターンの一例です。
チームの人数・技術レベル・プロジェクトの性質に応じて、適宜カスタマイズして取り入れてください。

構成方針

レイヤーベースの構成は、アプリケーションの機能を役割ごと(レイヤー)に分割する方法です。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>');
});

おわりに

この構成は、以下のような開発スタイルにフィットします

  • 少人数チームや経験の浅いメンバーを含む開発体制
  • ドメインが複雑で、ロジックの見通しや整理が重要なプロジェクト
  • 初期は小さく始め、段階的に機能を追加していくアプローチ

構成は一度決めたら終わりではなく、育てていくもの。

現在の最適解が未来のボトルネックにならないよう、シンプルな原則を軸に柔軟な見直しを続けていきましょう。

また、本ディレクトリ構成例は、あくまで一例になりますので、言語やフレームワークに適した構成に適宜変換してください。

ブログ一覧へ戻る

お気軽にお問い合わせください

SREの設計・技術支援から、
SRE運用内で使用する
ツールの導入など、
SRE全般についてご支援しています。

資料請求・お問い合わせ