mcpField
M
modular-architecture

modular-architecture

github.com/surajkuushwaha開発037 回インストール12 週間前に更新
リポジトリ

modular-architecture について

modular-architectureは、TypeScriptプロジェクトでAPIやバックエンド機能を構成する開発者向けのコーディングスキルです。厳密なレイヤーの責務、機能単位のモジュール、リポジトリの配置、明示的な公開エクスポートを定めています。

主な機能

  1. 01ルート、コントローラー、サービス、リポジトリを分離
  2. 02機能単位でコードをモジュール化
  3. 03リポジトリの注入と登録方法を規定
  4. 04スキーマ、型、ヘルパーの責務を定義
  5. 05モジュールの公開エクスポートを明示

活用シーン

  1. 01明確なレイヤー構成でバックエンド機能を追加
  2. 02コードを機能モジュールへリファクタリング
  3. 03データベースアクセスをリポジトリに集約
  4. 04モジュール間のインポートとエクスポートを確認

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/surajkuushwaha/skills --skill modular-architecture

インストール後、Claude Code で/modular-architectureを入力して読み込まれたか確認します。

詳細

使用するタイミング

  • 新しい API/バックエンド機能
  • クリーンなレイヤー構造へのリファクタリング
  • 関心事の分離の徹底

アーキテクチャ

フロー:route → controller → service → repository。レイヤーは厳密に分け、重複させません。 機能ごとのサイドファイル:schemas(バリデーション)、types(TS の型/インターフェース)、config(機能の定数/設定)、utils または helpers(純粋なヘルパー)、crons(スケジュールされたジョブ)、index.ts(バレル)。

フォルダー名は複数形にします:controllers/、services/、schemas/、helpers/、utils/、crons/。モジュールごとに utils/ または helpers/ を選び、一貫性を保ってください。

すべてのリポジトリは packages/cx-datastore/src/repositories/ に配置します。src/modules/ の中には配置しないでください。

フォルダー構成

レイヤーではなく、機能ごとにグループ化します。1 つのモジュールにつき 1 つのフォルダーです。

src/
└── modules/
    ├── user/
    │   ├── index.ts                    # public barrel — export only what's needed
    │   ├── controllers/
    │   │   └── user.controller.ts
    │   ├── services/
    │   │   └── user.service.ts
    │   ├── schemas/
    │   │   └── user.schema.ts
    │   ├── types/
    │   │   └── user.types.ts
    │   ├── config/
    │   │   └── user.config.ts
    │   ├── crons/                      # scheduled jobs — repo + helper only
    │   │   └── user.cron.ts
    │   └── utils/                      # also named helpers/ — both valid
    │       └── user.util.ts
    └── order/
        └── ... (same shape)

packages/
└── cx-datastore/
    └── src/
        └── repositories/
            ├── index.ts                # aggregates all domain repos
            ├── user/
            │   ├── index.ts            # exports domain repo classes
            │   └── user.repository.ts
            └── campaign_report/
                ├── index.ts
                └── campaign_report.repository.ts

src/shared/db/
    ├── index.ts                        # initializes models via initModels()
    ├── sequelize.ts                    # sequelize connection
    └── repositories.ts                 # instantiates all repo classes with models

命名規則:

  • リポジトリ:packages/cx-datastore/src/repositories/<domain>/<feature>.repository.ts
  • <feature>:snake_case、単数形(userProfile/user-profile/users ではなく user_profile)
  • <domain>:関連するリポジトリをまとめる snake_case のフォルダー(campaign_report、analytics)
  • フォルダー名:常に snake_case(campaign_report/、user_profile/)
  • ファイル名:常に snake_case(user_profile.repository.ts、user_profile.service.ts)
  • 例外 — model ファイルのみ:PascalCase(UserProfile.model.ts、CampaignReport.model.ts)

レイヤーの責務

Route

RESTful エンドポイントを定義します。ロジックは含めません。controller のみを呼び出します。

router.post("/users", createUserController);

Controller

req/res を処理します。入力を検証します。route と説明を jsDoc で記述します。ロジックは service に委譲します。複数の route がある場合は、すべてを記載します。

/**
 * @route POST /users
 * @description Create a new user
 */
export const createUserController = async (req, res) => {
  const result = await createUserService(req.body);
  res.json(result);
};

Service

ビジネスロジックを担当します。HTTP は扱いません。データ取得には repository を呼び出します。util を使用することもできます。

export const createUserService = async (data) => {
  return await userRepository.create(data);
};

Repository

DB 操作のみを行います。ビジネスロジックは含めません。req/res は扱いません。すべてのリポジトリは packages/cx-datastore/src/repositories/<domain>/ に配置します。

  • クラスをエクスポートします(インスタンスではありません) — export default FooRepository
  • コンストラクターは models: ModelsType(および任意で sequelize: Sequelize)を受け取ります
  • ModelsType = Pick<InitializedModels, "Model1" | "Model2"> — このリポジトリで必要なモデルのみを指定します
  • リポジトリファイル自体ではインスタンス化しません
  • src/shared/db/repositories.ts で一度だけインスタンス化します

手順 1 — repository クラスを作成します:

// packages/cx-datastore/src/repositories/user/user.repository.ts
import { Model, Transaction } from "sequelize";
import type { InitializedModels } from "../../index";

type ModelsType = Pick<InitializedModels, "User">;

class UserRepository {
  private models: ModelsType;

  constructor(models: ModelsType) {
    this.models = models;
  }

  async create(
    data: Record<string, unknown>,
    transaction?: Transaction,
  ): Promise<Model> {
    return this.models.User.create(data, {
      ...(transaction ? { transaction } : {}),
    });
  }

  async findById(id: number): Promise<Model | null> {
    return this.models.User.findByPk(id);
  }
}

export default UserRepository;

手順 2 — domain の index.ts に追加します:

// packages/cx-datastore/src/repositories/user/index.ts
import UserRepository from "./user.repository";

const userRepositories = {
  UserRepository,
};

export default userRepositories;

手順 3 — ルートの repositories/index.ts に登録します:

// packages/cx-datastore/src/repositories/index.ts
import { default as campaignReportRepositories } from "./campaign_report";

const repositories = {
  campaignReport: campaignReportRepositories,
};

export default repositories;

手順 4 — src/shared/db/repositories.ts でインスタンス化します:

// src/shared/db/repositories.ts
import { repositories } from "@culturex-art/datastore";
import { models } from "./index";
import { sequelize } from "./sequelize";

export const userRepository = new repositories.user.UserRepository({
  User: models.User,
});

// Pass sequelize as 2nd arg when repo needs raw queries or transactions:
export const campaignReportRepository =
  new repositories.campaignReport.CampaignReportRepository(
    { Campaign: models.Campaign, CampaignReport: models.CampaignReport },
    sequelize,
  );

手順 5 — service でインポートします:

import { userRepository } from "../../../shared/db/repositories";

Schema

バリデーションスキーマ、DTO、共有型を定義します。ランタイムロジック、I/O、副作用は含めません。controller(検証用)、service(型用)、repository(エンティティの形状用)からインポートします。

import { z } from "zod";

export const createUserSchema = z.object({
  email: z.string().email(),
  name: z.string().min(1),
});

export type CreateUserInput = z.infer<typeof createUserSchema>;

Types

機能にスコープを限定した TypeScript の型とインターフェースを定義します。ランタイムロジックや I/O は含めません。schema とは分けます。schema はバリデーションと推論される型、types はバリデーターに紐付かない単独のインターフェース/enum です。

// user/types/user.types.ts
export interface UserSummary {
  id: number;
  displayName: string;
  role: UserRole;
}

export enum UserRole {
  Admin = "admin",
  Member = "member",
}

Config

機能にスコープを限定した定数と設定値を定義します。ランタイムロジック、I/O、ビジネス上の判断は含めません。

// user/config/user.config.ts
export const USER_CONFIG = {
  maxNameLength: 100,
  defaultRole: "member",
  sessionTtlSeconds: 3600,
} as const;

Cron

スケジュールされたジョブを扱います。service と同様の構造で、定期タスクのビジネスロジックを担います。 インポートできるのは repository、helper/util のみです。 HTTP、controller、service、route は使用できません。 service のロジックが必要ですか?helper/util に切り出すか、両方で使える共通部分を helper に移してください。

// user/crons/user.cron.ts
import { userRepository } from "../../../shared/db/repositories";
import { formatUserDisplayName } from "../utils/user.util";

export const purgeStaleUsersCron = async () => {
  const stale = await userRepository.findStale();
  for (const u of stale) {
    await userRepository.softDelete(u.id);
  }
};

Util / Helper

フォルダー名は utils/ または helpers/ のどちらでもかまいません。モジュールごとに一方を選び、一貫性を保ってください。

純粋でステートレスな、機能にスコープを限定した処理を記述します。HTTP、DB、ビジネス上の判断は扱いません。 Service → Util は可。Util → Service/Controller/Repository/Route は禁止です。 Helper にロジックや I/O が必要ですか?service に移してください。

export const formatUserDisplayName = (firstName: string, lastName: string) => {
  return `${firstName} ${lastName}`.trim();
};

モジュールのエクスポート(index.ts)

各モジュールフォルダーには index.ts があり、他のモジュールが必要とするものだけを再エクスポートします。無差別に export * することはありません。内部要素(utils、crons、内部ヘルパー、プライベート型)は、他のモジュールで正当に必要とされない限り、エクスポートしません。

// src/modules/user/index.ts — named, explicit, minimal
export { userRoutes } from "./routes/user.routes";
export { createUserService, getUserService } from "./services/user.service";
export { createUserSchema, type CreateUserInput } from "./schemas/user.schema";
// controllers, crons, utils, config NOT exported — internal

ルール:

  • 他のモジュールが使用する公開インターフェースだけをエクスポートします。無差別な export * による再エクスポートは禁止です
  • コントローラー、crons、utils/ヘルパー、config はデフォルトで内部扱いです
  • モジュール間のインポートには index.ts を経由します(import { createUserService } from "@/modules/user")
  • モジュール内のインポートには相対パスを使用します
  • 新たに公開する必要があるものは、名前付きエクスポートとして意図的に追加します

実装手順

  1. src/modules/<feature>/ を作成します
  2. types/<feature>.types.ts — 単独の TS 型/インターフェース/列挙型(必要な場合)
  3. config/<feature>.config.ts — 機能の定数(必要な場合)
  4. schemas/<feature>.schema.ts — バリデーションスキーマと推論された型
  5. packages/cx-datastore/src/repositories/<domain>/<feature>.repository.ts — クラス、コンストラクターインジェクション、export default ClassName
  6. packages/cx-datastore/src/repositories/<domain>/index.ts — ドメインオブジェクトに追加します
  7. packages/cx-datastore/src/repositories/index.ts — 新しいドメインの場合は追加します
  8. src/shared/db/repositories.ts — new repositories.<domain>.FooRepository({ ...models }) でインスタンスを生成します
  9. services/<feature>.service.ts — shared/db/repositories から名前付きインスタンスをインポートします
  10. utils/<feature>.util.ts または helpers/<feature>.helper.ts — 純粋なヘルパー(必要な場合)
  11. crons/<feature>.cron.ts — スケジュールされたジョブ。リポジトリとヘルパーのみ(必要な場合)
  12. controllers/<feature>.controller.ts — jsDoc
  13. routes/<feature>.routes.ts — RESTful エンドポイント
  14. index.ts — 公開インターフェースのみを再エクスポートします(名前付きエクスポート。無差別な export * は禁止)

ルール(厳守)

  • DB アクセス:リポジトリ経由のみ。必ず packages/cx-datastore/src/repositories/ に配置します
  • src/modules/ 内にリポジトリを配置してはいけません
  • リポジトリ:モデルをコンストラクターインジェクションし、クラスをエクスポートします(インスタンスではありません)。インスタンスの生成場所は src/shared/db/repositories.ts のみに限定します
  • コントローラーにビジネスロジックを記述しません
  • ルートからリポジトリを直接呼び出してはいけません
  • サービスは HTTP に依存せず、再利用可能にします
  • Util/ヘルパーからサービス/コントローラー/リポジトリ/ルートをインポートすることは禁止です(一方向のみ:サービス → Util/ヘルパー)
  • Util フォルダーには utils/ または helpers/ を使用します。モジュールごとにどちらか一方を選び、統一してください
  • フォルダー名は常に複数形にします:controllers/、services/、schemas/、helpers//utils/、crons/
  • Cron がインポートできるのはリポジトリとヘルパー/Util のみです。 サービス/コントローラー/ルートは禁止です
  • スキーマはバリデーションと推論された型のみを扱い、実行時ロジックを含めません
  • 型は単独のインターフェース/列挙型のみを扱い、実行時ロジックや I/O を含めません
  • Config は定数のみを扱い、実行時ロジックや I/O を含めません
  • モジュール間のアクセスには index.ts のバレルのみを経由します
  • index.ts からエクスポートするのは必要なものだけにします。名前付きエクスポートを使用し、無差別な export * は禁止です
  • コードでは async/await を一貫して使用し、モジュール化して読みやすくします

アンチパターン

  • ロジックを内包する肥大化したコントローラー
  • req/res に触れるサービス
  • ビジネスルールを含むリポジトリ
  • レイヤー間の密結合
  • 別のモジュールの内部ファイルに直接アクセスすること(index.ts を迂回する)
  • リポジトリファイル内の export default new FooRepository() — ここでインスタンスを生成してはいけません
  • src/modules/ 内に置かれたリポジトリ — 必ず cx-datastore に配置します
  • サービス/コントローラーを呼び出す Cron — 代わりに共通ロジックをヘルパーに切り出します
  • 単数形のフォルダー名(controller/、service/)— 必ず複数形にします
  • モジュールの index.ts にある export * — 内部要素が漏れるため、名前付きエクスポートを使用します

相性の良いもの

  • cx-tdd
  • cx-improve-architecture
  • cx-domain-model

コメント