ディレクトリ構成 ~ハイブリッド型編~

Kodai Nakahara

2025.10.16

はじめに

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

本記事では、ディレクトリ構成に悩む現場に向けて、ハイブリッド型構成をご紹介します。

⚠️ この構成が「唯一の正解」ではありません。

本記事は、筆者が実案件・チーム開発の中で得た知見をもとに「比較的扱いやすく、現実的に運用しやすい」と感じた構成パターンの一例です。

チームの人数・技術レベル・プロジェクトの性質に応じて、適宜カスタマイズして取り入れてください。

構成方針

  • 機能(feature)と役割(layer)の両軸でディレクトリを構成
  • 各機能ごとにコンポーネント・ロジック・状態をまとめつつ、共通処理はレイヤーとして整理
  • アプリの規模やドメインの複雑さに応じて、機能内にレイヤー構造を持たせる/共通領域に抽出するなど柔軟に設計
  • 横断的な処理は shared/core/ に集約し、責務を分離しながら全体の見通しを保つ

メリット

  • レイヤーベースのシンプルさと、フィーチャーベースのスケーラビリティを両立
    役割ごとの整理と、機能単位の分離の両方を実現。
  • 大規模プロジェクトでも開発チームごとに適切に分割できる
    並行開発しやすく、チームごとに管理が可能。
  • コードの再利用性が高く、保守性に優れる
    共通のロジックを適切に分割し、開発効率を向上させる。

デメリット

  • 最適なバランスを見つけるのに時間がかかる
    設計段階での判断が重要で、適切な分割方法を決めるのに時間を要する。
  • プロジェクトの成長に応じた構成の見直しが必要
    初期の設計が適切でないと、プロジェクトの拡大とともに構成が破綻する可能性がある。

レイヤー / フィーチャー の分け方の基準

「どこまでを feature として独立させるか」「どこまでを layer として共通化するか」は、構成全体の保守性やスケーラビリティに大きく影響します。

それぞれの分け方の基準は各ディレクトリ構成の記事を参照してください。

💡 基本は「1つの機能で完結する構造」を目指しつつ、

共通化できるロジックはレイヤーとして分離し、柔軟性と再利用性を高めるのがおすすめです。

ディレクトリ構成例

フロントエンド

src/
├── core/                           // アプリ全体の基盤処理(レイヤー構造)
│   ├── api/                        // APIクライアント設定
│   │   └── axiosClient.ts
│   ├── auth/                       // 認証処理
│   │   ├── authProvider.ts
│   │   └── token.ts
│   ├── config/                     // 環境設定
│   │   └── env.ts
│   ├── error/                      // 共通のエラーハンドリング
│   │   └── errorHandler.ts
│   ├── store/                      // グローバル状態の初期化・永続化
│   │   └── globalStore.ts
│   └── index.ts                    // 初期化のエントリーポイント
│
├── shared/                         // 共通の再利用部品(レイヤー構造)
│   ├── components/                 // 共通UI(Button, Modal など)
│   │   ├── Button.tsx
│   │   └── Modal.tsx
│   ├── hooks/                      // 汎用フック(useDebounce など)
│   │   └── useDebounce.ts
│   ├── utils/                      // 純関数群
│   │   └── formatDate.ts
│   ├── constants/                  // 定数
│   │   └── app.ts
│   └── types/                      // 共通型定義
│       └── index.ts
│
├── features/                       // 機能単位で分離(内部でレイヤー構造)
│   ├── user/
│   │   ├── components/             // 機能専用のUI
│   │   ├── hooks/                  // 状態取得・副作用
│   │   ├── usecases/               // アプリケーションロジック
│   │   ├── repositories/           // APIやローカル取得
│   │   ├── store/                  // 状態管理(Zustandなど)
│   │   └── index.ts                // エクスポート
│   ├── product/
│   └── order/
│
├── pages/                          // 複数featureを組み合わせる画面UI
│   ├── user.tsx
│   └── dashboard.tsx
│
└── main.tsx                        // アプリのエントリーポイント

💡 設計のポイント

  • features/ 各機能は、UI・状態・ロジックを自己完結
  • core/ はグローバルな初期化・設定・API クライアント管理
  • shared/ は完全な横断的再利用対象(共通 UI・hooks・utils など)
  • pages/ は機能の組み合わせによる画面構築
  • core/ shared/ features/ いずれも内部を責務単位に分けたレイヤー構成にすることで、構造が破綻しにくく、保守性が向上します。

UI の組み立て例

// 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} />;
};
// 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/
├── core/                           // アプリ全体の基盤機能(レイヤー構造)
│   ├── config/                     // 環境設定・定数
│   │   └── env.ts
│   ├── database/                   // DB接続・初期化
│   │   └── connection.ts
│   ├── logger/                     // ログ出力
│   │   └── logger.ts
│   ├── auth/                       // 認証・JWT関連
│   │   └── verifyToken.ts
│   ├── error/                      // 共通エラーハンドリング
│   │   └── errorHandler.ts
│   └── index.ts                    // アプリ初期化用
│
├── shared/                         // 共通部品(型・ユーティリティ)
│   ├── utils/                      // 汎用関数
│   │   └── date.ts
│   ├── constants/                  // 定数
│   │   └── roles.ts
│   └── types/                      // 型定義
│       └── user.ts
│
├── features/                       // 機能ごとの構成(内部にレイヤーあり)
│   ├── user/
│   │   ├── usecases/               // ビジネスロジック
│   │   ├── repositories/           // DB操作・外部API通信
│   │   ├── models/                 // ORMモデル
│   │   ├── schema/                 // バリデーションスキーマ
│   │   └── index.ts                // エクスポート
│   ├── product/
│   └── order/
│
├── routers/                        // APIルーティング
│   ├── user/route.ts
│   ├── product/route.ts
│   └── dashboard/route.ts
│
├── main.ts                         // サーバー起動・ルーティング統合
└── tsconfig.json

💡 設計のポイント

  • features/* 各機能内で usecase / repository / schema などを内包し完結
  • routers/ から usecase を直接呼び出し、controller 層は不要
  • 共通初期化・認証・DB接続などは core/ に集約
  • 型や定数、純関数は shared/ にまとめて再利用しやすく
  • core/ shared/ features/ いずれも内部にレイヤーを持つことで、役割が明確になり構造が破綻しづらい

API の組み立て例

// routers/user/route.ts
import { Router } from 'express';
import { getUserById } from '@/features/user/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 '@/features/user/usecases/userUsecase';
import { getOrdersByUserId } from '@/features/order/usecases/orderUsecase';
import { getProductsByIds } from '@/features/product/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;

ルーティングの結合例

// routers/dashboard/route.ts
import { Router } from 'express';
import { getUserById } from '@/features/user/usecases/userUsecase';
import { getOrdersByUserId } from '@/features/order/usecases/orderUsecase';
import { getProductsByIds } from '@/features/product/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;

おわりに

ハイブリッド型構成は、レイヤーの明確さ機能単位での保守性・拡張性を両立した設計です。

以下のようなケースに適しています

  • 中〜大規模プロジェクトで機能、責務問わず複数のメンバーが同時並行で開発する
  • 機能と責務の分離を同時に行いたい
  • 拡張性・再利用性を高く保ちながら、柔軟に機能追加を行いたい

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

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

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

ブログ一覧へ戻る

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

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

資料請求・お問い合わせ