重构规范总结
分类:elysia
发布于:
阅读时间:101 分钟
Elysia + Drizzle + Zod 全栈重构规范总结
概述
本文档基于最新的 Elysia + Drizzle + Zod 全栈架构,总结项目重构的关键规范和最佳实践,主要涵盖:
- RESTful API 接口命名规范
- Drizzle + Zod 四层架构规范
- Service层错误处理规范
- Controller层统一响应格式
- 类型系统最佳实践
一、RESTful API 接口命名规范
1.1 URL路径规范
正确示例:
// ✅ 正确:使用复数名词,层级清晰
GET /api/users // 获取用户列表
GET /api/users/:id // 获取单个用户
POST /api/users // 创建用户
PUT /api/users/:id // 更新用户
DELETE /api/users/:id // 删除用户
// ✅ 正确:嵌套资源
GET /api/users/:id/orders // 获取用户的订单列表
POST /api/users/:id/orders // 为用户创建订单
错误示例:
// ❌ 错误:使用动词
GET /api/getUsers
POST /api/createUser
PUT /api/updateUser
// ❌ 错误:使用单数名词
GET /api/user
POST /api/user
// ❌ 错误:不规范的嵌套
GET /api/getUserOrders/:id
1.2 HTTP方法规范
| HTTP方法 | 用途 | 示例 |
|---|---|---|
| GET | 获取资源 | GET /api/users |
| POST | 创建资源 | POST /api/users |
| PUT | 完整更新资源 | PUT /api/users/:id |
| PATCH | 部分更新资源 | PATCH /api/users/:id |
| DELETE | 删除资源 | DELETE /api/users/:id |
1.3 方法命名规范
Service层方法命名:
// ✅ 正确:清晰的业务语义
class UserService {
async getUserList(query: UserQueryDto) { }
async getUserById(id: number) { }
async createUser(data: CreateUserDto) { }
async updateUser(id: number, data: UpdateUserDto) { }
async deleteUser(id: number) { }
}
Controller层路由定义:
// ✅ 正确:RESTful风格
export const userController = new Elysia({ prefix: '/users' })
.get('/', async ({ query, userService }) => {
const result = await userService.getUserList(query);
return commonRes(result);
})
.get('/:id', async ({ params, userService }) => {
const user = await userService.getUserById(params.id);
return commonRes(user);
});
二、Drizzle + Zod 四层架构规范
2.1 架构层级
src/modules/user/
├── user.schema.ts # 第1层:Drizzle表定义
├── user.zod.ts # 第2层:Zod Schema校验
├── user.model.ts # 第3层:业务模型定义
└── user.types.ts # 第4层:TypeScript类型
2.2 第1层:Drizzle表定义
// user.schema.ts
import { pgTable, serial, varchar, timestamp, boolean } from 'drizzle-orm/pg-core';
export const userTable = pgTable('users', {
id: serial('id').primaryKey(),
username: varchar('username', { length: 50 }).notNull().unique(),
email: varchar('email', { length: 100 }).notNull().unique(),
isActive: boolean('is_active').default(true),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
});
2.3 第2层:Zod Schema校验
// user.zod.ts
import { createInsertSchema, createSelectSchema } from 'drizzle-zod';
import { userTable } from './user.schema';
import { z } from 'zod';
// 基础Schema
export const insertUserSchema = createInsertSchema(userTable, {
email: z.string().email('请输入有效的邮箱地址'),
username: z.string().min(2, '用户名至少2个字符').max(50, '用户名最多50个字符'),
});
export const selectUserSchema = createSelectSchema(userTable);
// 业务Schema
export const createUserSchema = insertUserSchema.omit({
id: true,
createdAt: true,
updatedAt: true,
});
export const updateUserSchema = insertUserSchema.partial().omit({
id: true,
createdAt: true,
updatedAt: true,
});
export const userQuerySchema = z.object({
page: z.number().min(1).default(1),
limit: z.number().min(1).max(100).default(10),
search: z.string().optional(),
isActive: z.boolean().optional(),
});
2.4 第3层:业务模型定义
// user.model.ts
import { t, Static } from 'elysia';
import { createUserSchema, updateUserSchema, userQuerySchema, selectUserSchema } from './user.zod';
// Elysia模型定义
export const userModel = {
CreateUserDto: t.Object({
username: t.String({ minLength: 2, maxLength: 50 }),
email: t.String({ format: 'email' }),
isActive: t.Optional(t.Boolean()),
}),
UpdateUserDto: t.Partial(t.Object({
username: t.String({ minLength: 2, maxLength: 50 }),
email: t.String({ format: 'email' }),
isActive: t.Boolean(),
})),
UserQueryDto: t.Object({
page: t.Optional(t.Number({ minimum: 1, default: 1 })),
limit: t.Optional(t.Number({ minimum: 1, maximum: 100, default: 10 })),
search: t.Optional(t.String()),
isActive: t.Optional(t.Boolean()),
}),
UserModel: t.Object({
id: t.Number(),
username: t.String(),
email: t.String(),
isActive: t.Boolean(),
createdAt: t.Date(),
updatedAt: t.Date(),
}),
};
2.5 第4层:TypeScript类型
// user.types.ts
import { Static } from 'elysia';
import { userModel } from './user.model';
import { z } from 'zod';
import { createUserSchema, updateUserSchema, userQuerySchema, selectUserSchema } from './user.zod';
// 从Elysia模型导出类型
export type CreateUserDto = Static<typeof userModel.CreateUserDto>;
export type UpdateUserDto = Static<typeof userModel.UpdateUserDto>;
export type UserQueryDto = Static<typeof userModel.UserQueryDto>;
export type UserModel = Static<typeof userModel.UserModel>;
// 从Zod Schema导出类型
export type CreateUserInput = z.infer<typeof createUserSchema>;
export type UpdateUserInput = z.infer<typeof updateUserSchema>;
export type UserQueryInput = z.infer<typeof userQuerySchema>;
export type UserEntity = z.infer<typeof selectUserSchema>;
// 前端展示类型
export interface UserVo {
id: number;
username: string;
email: string;
isActive: boolean;
createdAt: string;
updatedAt: string;
}
三、Service层错误处理规范
3.1 禁止返回 null
Service层方法不应该返回 null,而应该抛出明确的错误
正确示例:
import { NotFoundError } from '@/utils/errors';
class UserService {
async getUserById(id: number): Promise<UserEntity> {
const [user] = await db
.select()
.from(userTable)
.where(eq(userTable.id, id))
.limit(1);
if (!user) {
throw new NotFoundError(`User with id ${id} not found`); // ✅ 抛出错误
}
return user; // ✅ 返回具体数据
}
}
错误示例:
// ❌ 错误:返回 null
async getUserById(id: number): Promise<UserEntity | null> {
const [user] = await db.select().from(userTable).where(eq(userTable.id, id)).limit(1);
return user || null; // ❌ 返回 null
}
3.2 标准分页返回格式
// ✅ 正确:标准分页格式
async getUserList(params: UserQueryDto) {
const { page = 1, limit = 10, search, isActive } = params;
// 构建查询条件
const conditions = [];
if (search) {
conditions.push(
or(
like(userTable.username, `%${search}%`),
like(userTable.email, `%${search}%`)
)
);
}
if (isActive !== undefined) {
conditions.push(eq(userTable.isActive, isActive));
}
// 构建查询
const queryBuilder = db.select().from(userTable);
const totalBuilder = db.select({ count: count() }).from(userTable);
if (conditions.length > 0) {
const whereCondition = and(...conditions);
queryBuilder.where(whereCondition);
totalBuilder.where(whereCondition);
}
// 分页
const offset = (page - 1) * limit;
queryBuilder.limit(limit).offset(offset);
// 并行查询
const [users, [{ count: total }]] = await Promise.all([
queryBuilder,
totalBuilder
]);
// 标准分页返回格式
return {
items: users, // 必须使用 items 字段名
meta: {
total,
page,
limit,
totalPages: Math.ceil(total / limit),
}
};
}
3.3 Service层最佳实践
- 无需数据验证 - 在 Elysia 的 Controller 已经验证过了
- 排除结果为null - 抛出错误而不是返回null
- 查询返回什么就返回什么 - 不做额外的数据转换
- 分页返回 {items, meta} 数据格式
- 使用 getTableColumns() 获取表字段
三、Partner 模式重构规范
3.1 模式定义
Partner 模式是指 Service 层方法直接返回数据或抛出错误,不使用包装的响应对象。
3.2 重构步骤
步骤1:移除 commonRes 包装
// 重构前
async findActiveUsers(): Promise<ServiceResponse<UserEntity[]>> {
// ...
return commonRes(users);
}
// 重构后
async getActiveUsers(): Promise<UserEntity[]> {
// ...
return users;
}
步骤2:方法重命名
遵循更清晰的命名约定:
findActiveUsers→getActiveUsersbatchUpdateStatus→updateStatusBatchfindByEmail→getByEmail
步骤3:更新控制器调用
// 重构前
const result = await usersService.findActiveUsers();
return result;
// 重构后
const users = await usersService.getActiveUsers();
return commonRes(users);
3.3 实施要点
- Service 层专注业务逻辑,不处理响应格式
- Controller 层负责统一响应格式
- 保持职责分离原则
四、Controller层统一响应格式
4.1 使用 commonRes 函数
所有Controller必须使用 commonRes 函数返回数据
正确示例:
import { commonRes } from '@/utils/response';
export const userController = new Elysia({ prefix: '/users' })
.model(userModel) // 注册模型
.get('/', async ({ query, userService }) => {
const result = await userService.getUserList(query);
return commonRes(result); // ✅ 正确
}, {
query: 'UserQueryDto'
})
.post('/', async ({ body, userService }) => {
const user = await userService.createUser(body);
return commonRes(user); // ✅ 正确
}, {
body: 'CreateUserDto'
});
错误示例:
// ❌ 错误:直接返回对象
return { success: true, data: users };
// ❌ 错误:直接返回数据
return users;
// ❌ 错误:自定义响应格式
return { code: 200, message: 'success', result: users };
4.2 Controller命名规范
- 使用
controller命名,不要使用route - 文件命名:
xxx.controller.ts - 导出命名:
xxxController
4.3 Controller层简化
由于Service层不再返回null,Controller层无需进行null检查:
// ✅ 简化后的Controller
export const userController = new Elysia({ prefix: '/users' })
.get('/:id', async ({ params, userService }) => {
const user = await userService.getUserById(params.id);
return commonRes(user); // 无需null检查
});
五、数据库和API模型定义规范
5.1 目录结构
src/
├── db/
│ ├── schema/ # 数据库表结构定义
│ │ ├── index.schema.ts # 统一导出
│ │ └── [entity].schema.ts
│ ├── types/ # 类型转换层
│ │ └── database.typebox.ts # Drizzle → TypeBox 转换
│ └── connection.ts # 数据库连接实例
5.2 数据库Schema定义
- 使用 Drizzle ORM 定义数据库表结构
- 统一的时间戳字段:
createdAt、updatedAt - 主键使用自增ID:
id: integer('id').primaryKey({ autoIncrement: true })
5.3 类型导出方式(新规范)
不直接使用 userTable 导出类型,而是在每个 module 的 model 中创建需要的类型,防止实例化过深
database.types.ts 统一类型转换
// 第一步:分别定义所有 insert schemas
const userInsertSchema = createInsertSchema(dbSchema.userTable, {
email: t.Optional(t.String({ format: "email" })),
});
const partnersInsertSchema = createInsertSchema(dbSchema.partnersTable);
// 第二步:分别定义所有 select schemas
const userSelectSchema = createSelectSchema(dbSchema.userTable);
const partnersSelectSchema = createSelectSchema(dbSchema.partnersTable);
// 第三步:组合对象
const insertSchemas = {
userTable: userInsertSchema,
partnersTable: partnersInsertSchema,
};
const selectSchemas = {
userTable: userSelectSchema,
partnersTable: partnersSelectSchema,
};
// 第四步:创建最终的 DbType 对象
export const DbType = {
typebox: {
insert: insertSchemas,
select: selectSchemas,
},
spreads: {
insert: spreads(insertSchemas, "insert"),
select: spreads(selectSchemas, "select"),
},
} as const;
5.4 Model 类型命名规范
标准命名模式:
CreateXxxDto- 创建数据传输对象UpdateXxxDto- 更新数据传输对象UpdateSortDto- 排序更新对象XxxQuery- 查询参数对象XxxModel- 实体模型类型(提供给前端展示信息的类型)
partners.model.ts 示例
import { DbType } from "@/db/database.types";
import { Static, t } from "elysia";
// 创建合作伙伴DTO
export const CreatePartnerDto = t.Omit(DbType.typebox.insert.partnersTable, [
"id",
"createdAt",
"updatedAt",
]);
// 更新合作伙伴DTO
export const UpdatePartnerDto = t.Partial(
t.Omit(DbType.typebox.insert.partnersTable, [
"id",
"createdAt",
"updatedAt",
])
);
// 查询参数
export const PartnerQueryDto = t.Object({
page: t.Optional(t.Number({ minimum: 1, default: 1 })),
limit: t.Optional(t.Number({ minimum: 1, maximum: 100, default: 10 })),
search: t.Optional(t.String()),
name: t.Optional(t.String()),
isActive: t.Optional(t.Boolean()),
sort: t.Optional(t.String({ default: "order" })),
order: t.Optional(t.Union([t.Literal("asc"), t.Literal("desc")], { default: "asc" })),
});
// 排序更新DTO
export const UpdateSortDto = t.Object({
order: t.Number({ minimum: 0 }),
});
// 导出 TypeScript 类型
export type CreatePartnerDto = Static<typeof CreatePartnerDto>;
export type UpdatePartnerDto = Static<typeof UpdatePartnerDto>;
export type PartnerQueryDto = Static<typeof PartnerQueryDto>;
export type UpdateSortDto = Static<typeof UpdateSortDto>;
// 实体模型类型(给前端使用)
export type PartnerModel = Static<typeof DbType.typebox.select.partnersTable>;
六、API 模型定义规范
6.1 基础模型复用
export const userModel = {
// 直接使用数据库查询类型
user: DbType.typebox.select.user,
// 使用 Pick 选择需要的字段
userBasic: t.Pick(DbType.typebox.select.user, ['id', 'username']),
// 使用 Omit 排除不需要的字段
updateUser: t.Omit(DbType.typebox.insert.user, ['id', 'updatedAt', 'createdAt']),
}
6.2 路由类型应用
export const userRoutes = new Elysia({ prefix: '/users' })
.model(userModel) // 注册模型
.post('/users', async ({ body, usersService }) => {
const user = await usersService.create(body);
return commonRes(user);
}, {
body: 'updateUser' // 使用模型中定义的类型
})
七、Service层规范
7.1 Service层主要特点
参考 partners.service.ts 的实现模式:
- 无需数据验证 - 在 Elysia 的 Controller 已经验证过了
- 排除结果为null - 抛出错误而不是返回null
- 查询返回什么就返回什么 - 不做额外的数据转换
- 分页返回 {items, meta} 数据格式
- 使用 getTableColumns() 获取表字段
7.2 分页返回格式(标准)
必须按照以下方式实现分页:
// 分页查询实现
async getPartnersList(params: PartnerQueryDto) {
const {
page = 1,
limit = 10,
sort = "order",
order = "asc",
search,
name,
isActive,
} = params;
// 搜索条件构建
const conditions = [];
if (search) {
conditions.push(
or(
like(partnersTable.name, `%${search}%`),
like(partnersTable.description, `%${search}%`),
),
);
}
// 构建查询
const queryBuilder = db
.select({
...this.columns,
image: imagesTable.url,
})
.from(partnersTable)
.leftJoin(imagesTable, eq(partnersTable.image_id, imagesTable.id))
.orderBy(orderBy);
// 获取总数
const totalBuilder = db
.select({ count: count() })
.from(partnersTable);
if (conditions.length > 0) {
queryBuilder.where(and(...conditions));
totalBuilder.where(and(...conditions));
}
// 分页
const offset = (page - 1) * limit;
queryBuilder.limit(limit).offset(offset);
// 并行查询
const [partners, [{ count: total }]] = await Promise.all([queryBuilder, totalBuilder]);
// 标准分页返回格式
return {
items: partners, // 禁止使用其他字段名
meta: {
total,
page,
limit,
totalPages: Math.ceil(total / limit),
}
};
}
重要:分页返回格式禁止使用 items 以外的字段名,因为分页返回统一使用这个格式
7.3 错误处理规范
// 查询单个实体时,不存在则抛出错误
async getPartnerById(id: number) {
const partner = await db
.select({
...this.columns,
image: imagesTable.url,
})
.from(partnersTable)
.leftJoin(imagesTable, eq(partnersTable.image_id, imagesTable.id))
.where(eq(partnersTable.id, id))
.limit(1);
if (!partner.length) {
throw new NotFoundError(`Partner with id ${id} not found`);
}
return partner[0];
}
八、Controller层规范
8.1 命名规范
- 使用
controller命名,不要使用route - 文件命名:
xxx.controller.ts - 导出命名:
xxxController
8.2 返回格式规范
- 所有Controller必须使用
commonRes返回数据 - 禁止直接返回对象
// ✅ 正确
return commonRes(result);
// ❌ 错误
return { success: true, data: result };
return result;
8.3 Model类型使用
Controller应该使用Model中提供的类型进行验证:
export const partnersController = new Elysia({ prefix: '/partners' })
.model(partnersModel) // 注册模型
.post('/', async ({ body, partnersService }) => {
const partner = await partnersService.createPartner(body);
return commonRes(partner);
}, {
body: 'CreatePartnerDto' // 使用模型中定义的类型
})
.get('/', async ({ query, partnersService }) => {
const result = await partnersService.getPartnersList(query);
return commonRes(result);
}, {
query: 'PartnerQueryDto'
});
九、重构检查清单
9.1 API响应格式检查
- 所有Controller都导入了
commonRes - 所有返回都使用
commonRes(data)格式 - 没有直接返回
{ success: true, data: ... }的情况
9.2 Service层错误处理检查
- 所有可能返回null的方法都改为抛出
NotFoundError - 方法返回类型去掉了
| null - Controller中移除了不必要的null检查
- 分页返回格式使用
{items, meta}结构 - 禁止使用
items以外的字段名作为数据容器
9.3 Model类型定义检查
- 使用标准命名:
CreateXxxDto、UpdateXxxDto、XxxQuery、XxxModel - 从
DbType.typebox中派生类型,不直接使用schema - 导出TypeScript类型供Service和Controller使用
- 额外导出
XxxModel实体类型给前端使用
9.4 Controller命名和结构检查
- 使用
controller命名,不使用route - 文件命名:
xxx.controller.ts - 导出命名:
xxxController - 使用Model中提供的类型进行验证
- 所有返回都使用
commonRes
9.5 数据库类型转换检查
-
database.types.ts按步骤组织:分别定义→组合对象→创建DbType - 防止实例化过深,不直接从schema导出类型
- 使用
spreads工具函数处理类型展开
十、最佳实践总结
10.1 分层职责
- Service 层:纯业务逻辑,直接返回数据或抛出错误
- Controller 层:处理 HTTP 请求,使用
commonRes包装响应 - Model 层:类型定义和验证
- Service层专注业务逻辑,Controller层专注路由和验证
10.2 错误处理
- Service 层抛出具体错误
- Controller 层统一错误格式
- 前端统一错误处理
10.3 类型安全
- 利用 TypeScript 类型推断
- 使用 TypeBox 进行运行时验证
- 避免
any类型的使用 - 在每个模块的model中定义具体的DTO类型
10.4 分页实现标准
- 统一使用
{items, meta}返回格式 - meta包含:
total、page、limit、totalPages - 支持搜索、排序、过滤参数
- 使用条件构建器动态组装查询
- 使用
getTableColumns()获取表字段 - 并行查询数据和总数:
Promise.all([queryBuilder, totalBuilder])
十一、命名规范总结
11.1 Drizzle + Zod 四层架构命名规范
| 层级 | 文件命名 | 导出内容 | 命名规范 |
|---|---|---|---|
| 第1层 | xxx.schema.ts | xxxTable | 表名使用复数形式 |
| 第2层 | xxx.zod.ts | insertXxxSchema, selectXxxSchema | Schema后缀 |
| 第3层 | xxx.model.ts | xxxModel | Model后缀 |
| 第4层 | xxx.types.ts | XxxDto, XxxVo | DTO/VO后缀 |
11.2 业务层命名规范
| 层级 | 文件命名 | 类命名 | 方法命名 |
|---|---|---|---|
| Service层 | xxx.service.ts | XxxService | getXxxList, getXxxById, createXxx, updateXxx, deleteXxx |
| Controller层 | xxx.controller.ts | xxxController | RESTful路由:get('/'), get('/:id'), post('/') |
11.3 类型命名规范
| 类型用途 | 命名格式 | 示例 |
|---|---|---|
| 创建DTO | CreateXxxDto | CreateUserDto |
| 更新DTO | UpdateXxxDto | UpdateUserDto |
| 查询参数 | XxxQueryDto | UserQueryDto |
| 实体模型 | XxxModel | UserModel |
| 前端展示 | XxxVo | UserVo |
| 数据库实体 | XxxEntity | UserEntity |
十二、工具和命令
12.1 搜索和检查命令
# 检查RESTful接口命名规范
ripgrep "(get|post|put|delete).*[A-Z]" src/modules/ # 检查是否有驼峰命名的路由
# 检查Controller命名规范
find src/modules -name "*.route.ts" | wc -l # 应该为0
find src/modules -name "*.controller.ts" | wc -l
# 检查Service方法命名
ripgrep "async (get|create|update|delete)[A-Z]" src/modules/
# 检查分页返回格式
ripgrep "return.*{.*data.*:" src/modules/ # 应该使用items字段
# 检查commonRes使用
ripgrep "return.*commonRes" src/modules/
12.2 类型检查命令
# 验证Drizzle + Zod架构
find src/modules -name "*.schema.ts" -o -name "*.zod.ts" -o -name "*.model.ts" -o -name "*.types.ts"
# 验证类型命名规范
ripgrep "export.*Dto|export.*Vo|export.*Model|export.*Entity" src/modules/
# 验证Service返回类型(不应该有null)
ripgrep "Promise<.*\| null>" src/modules/
# 验证Controller使用commonRes
ripgrep "import.*commonRes" src/modules/
12.3 开发工具命令
# 运行类型检查
npm run type-check
# 运行测试
npm run test
# 代码格式化
npm run format
# 启动开发服务器
npm run dev
十三、迁移指南
13.1 从旧架构迁移步骤
-
创建四层架构文件
# 为每个模块创建四层文件 touch src/modules/user/user.schema.ts touch src/modules/user/user.zod.ts touch src/modules/user/user.model.ts touch src/modules/user/user.types.ts -
迁移数据库定义
- 将现有的表定义移动到
xxx.schema.ts - 使用标准的Drizzle定义格式
- 将现有的表定义移动到
-
创建Zod Schema
- 使用
createInsertSchema和createSelectSchema - 添加业务验证规则
- 使用
-
更新Service层
- 移除返回null的方法
- 统一分页返回格式
- 使用标准错误处理
-
更新Controller层
- 重命名为
xxx.controller.ts - 使用
commonRes包装响应 - 注册Elysia模型
- 重命名为
13.2 验证迁移结果
# 检查文件结构
find src/modules -type f -name "*.ts" | grep -E "(schema|zod|model|types|service|controller)\.ts$"
# 检查命名规范
ripgrep "export (const|class|type|interface)" src/modules/ | grep -v -E "(Dto|Vo|Model|Entity|Service|Controller|Schema|Table)$"
# 检查API规范
ripgrep "\.(get|post|put|delete)\(" src/modules/ | grep -v "^/"
注意:本规范基于 Elysia + Drizzle + Zod 全栈架构的实际重构经验总结,应根据项目发展持续更新和完善。