关于 modular-architecture
modular-architecture 是供开发者组织 API 和后端功能的编码技能,适用于 TypeScript 项目。它规定严格的分层职责、按功能划分的模块、仓库位置及显式导出规则。
主要功能
- 01划分路由、控制器、服务和仓库
- 02按功能组织代码模块
- 03规定仓库注入与注册方式
- 04明确 Schema、类型和辅助函数职责
- 05限制模块入口仅显式导出
适用场景
- 01分层清晰地添加后端功能
- 02将代码重构为功能模块
- 03将数据库访问集中在仓库层
- 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") - 模块内导入使用相对路径
- 需要公开新内容?有意添加具名导出
实现步骤
- 创建
src/modules/<feature>/ types/<feature>.types.ts——独立的 TS 类型/接口/枚举(如有需要)config/<feature>.config.ts——功能常量(如有需要)schemas/<feature>.schema.ts——验证模式及推导出的类型packages/cx-datastore/src/repositories/<domain>/<feature>.repository.ts——类、构造函数注入、export default ClassNamepackages/cx-datastore/src/repositories/<domain>/index.ts——添加到领域对象packages/cx-datastore/src/repositories/index.ts——如有新领域,则添加该领域src/shared/db/repositories.ts——使用new repositories.<domain>.FooRepository({ ...models })实例化services/<feature>.service.ts——从shared/db/repositories导入具名实例utils/<feature>.util.ts或helpers/<feature>.helper.ts——纯辅助函数(如有需要)crons/<feature>.cron.ts——定时任务,仅使用 repository 和辅助函数(如有需要)controllers/<feature>.controller.ts——jsDocroutes/<feature>.routes.ts——RESTful 端点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.tsbarrel 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