mcpField
M
modular-architecture

modular-architecture

github.com/surajkuushwaha开发037 次安装12 周前更新
代码仓库

关于 modular-architecture

modular-architecture 是供开发者组织 API 和后端功能的编码技能,适用于 TypeScript 项目。它规定严格的分层职责、按功能划分的模块、仓库位置及显式导出规则。

主要功能

  1. 01划分路由、控制器、服务和仓库
  2. 02按功能组织代码模块
  3. 03规定仓库注入与注册方式
  4. 04明确 Schema、类型和辅助函数职责
  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/,并保持一致。

所有 repository 都放在 packages/cx-datastore/src/repositories/。切勿放在 src/modules/ 中。

文件夹结构

按功能而非层级分组。一个模块对应一个文件夹。

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

命名规则:

  • Repository:packages/cx-datastore/src/repositories/<domain>/<feature>.repository.ts
  • <feature>:使用 snake_case,且为单数(user_profile,而不是 userProfile/user-profile/users)
  • <domain>:使用 snake_case 文件夹归纳相关 repo(campaign_report、analytics)
  • 文件夹名称:始终使用 snake_case(campaign_report/、user_profile/)
  • 文件名:始终使用 snake_case(user_profile.repository.ts、user_profile.service.ts)
  • 例外 — 仅模型文件:使用 PascalCase(UserProfile.model.ts、CampaignReport.model.ts)

层级职责

Route

定义 RESTful 端点。不包含逻辑。仅委托给 controller。

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

Controller

处理 req/res。验证输入。使用 jsDoc 编写路由及描述。将逻辑委托给 service。多个路由 → 全部记录。

/**
 * @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。所有 repo 都放在 packages/cx-datastore/src/repositories/<domain>/ 中。

  • 导出类(而不是实例)——export default FooRepository
  • 构造函数接受 models: ModelsType(以及可选的 sequelize: Sequelize)
  • ModelsType = Pick<InitializedModels, "Model1" | "Model2"> — 仅包含此 repo 所需的模型
  • 切勿在 repo 文件本身中实例化
  • 在 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

验证 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 = 不依赖验证器的独立接口/枚举。

// 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/辅助函数、配置:默认设为内部内容
  • 跨模块导入必须通过 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——定时任务,仅使用 repository 和辅助函数(如有需要)
  12. controllers/<feature>.controller.ts——jsDoc
  13. routes/<feature>.routes.ts——RESTful 端点
  14. index.ts——仅重新导出公共接口(具名导出,不使用一揽子的 export *)

规则(严格遵守)

  • 数据库访问:只能通过 repository,始终放在 packages/cx-datastore/src/repositories/
  • 绝不要将 repository 放在 src/modules/ 内
  • Repository:通过构造函数注入模型,导出类(而非实例),仅在 src/shared/db/repositories.ts 中实例化
  • 控制器中不得包含业务逻辑
  • Route 绝不能直接调用 repository
  • Services 与 HTTP 无关且可复用
  • Util/辅助函数不得导入 service/controller/repository/routes(单向依赖:service → util/helper)
  • Util 文件夹:命名为 utils/ 或 helpers/——每个模块选一种并保持一致
  • 文件夹名称始终使用复数:controllers/、services/、schemas/、helpers//utils/、crons/
  • Cron 只能导入:repository 和 helper/util。 不得导入 service/controller/route
  • Schema 仅包含验证和推导出的类型,不包含运行时逻辑
  • Types 仅包含独立接口/枚举,不包含运行时逻辑或 I/O
  • Config 仅包含常量,不包含运行时逻辑或 I/O
  • 跨模块访问只能通过 index.ts barrel
  • index.ts 仅导出必需内容——使用具名导出,不使用一揽子的 export *
  • 代码:统一使用 async/await,保持模块化且易读

反模式

  • 控制器臃肿并嵌入逻辑
  • Services 操作 req/res
  • Repository 包含业务规则
  • 各层之间紧密耦合
  • 直接访问其他模块的内部文件(绕过 index.ts)
  • 在 repo 文件中使用 export default new FooRepository()——绝不要在此处实例化
  • Repository 放在 src/modules/ 中——始终应属于 cx-datastore
  • Cron 调用 service/controller——应改为将共享逻辑提取到辅助函数
  • 文件夹名称使用单数(controller/、service/)——始终使用复数
  • 在模块的 index.ts 中使用 export *——这会暴露内部内容,应改用具名导出

配合使用效果良好

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

评论

支持 Markdown