返回首页

重构规范总结

分类: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层最佳实践

  1. 无需数据验证 - 在 Elysia 的 Controller 已经验证过了
  2. 排除结果为null - 抛出错误而不是返回null
  3. 查询返回什么就返回什么 - 不做额外的数据转换
  4. 分页返回 {items, meta} 数据格式
  5. 使用 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:方法重命名

遵循更清晰的命名约定:

  • findActiveUsersgetActiveUsers
  • batchUpdateStatusupdateStatusBatch
  • findByEmailgetByEmail

步骤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 定义数据库表结构
  • 统一的时间戳字段:createdAtupdatedAt
  • 主键使用自增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 的实现模式:

  1. 无需数据验证 - 在 Elysia 的 Controller 已经验证过了
  2. 排除结果为null - 抛出错误而不是返回null
  3. 查询返回什么就返回什么 - 不做额外的数据转换
  4. 分页返回 {items, meta} 数据格式
  5. 使用 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类型定义检查

  • 使用标准命名:CreateXxxDtoUpdateXxxDtoXxxQueryXxxModel
  • 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包含:totalpagelimittotalPages
  • 支持搜索、排序、过滤参数
  • 使用条件构建器动态组装查询
  • 使用 getTableColumns() 获取表字段
  • 并行查询数据和总数:Promise.all([queryBuilder, totalBuilder])

十一、命名规范总结

11.1 Drizzle + Zod 四层架构命名规范

层级文件命名导出内容命名规范
第1层xxx.schema.tsxxxTable表名使用复数形式
第2层xxx.zod.tsinsertXxxSchema, selectXxxSchemaSchema后缀
第3层xxx.model.tsxxxModelModel后缀
第4层xxx.types.tsXxxDto, XxxVoDTO/VO后缀

11.2 业务层命名规范

层级文件命名类命名方法命名
Service层xxx.service.tsXxxServicegetXxxList, getXxxById, createXxx, updateXxx, deleteXxx
Controller层xxx.controller.tsxxxControllerRESTful路由:get('/'), get('/:id'), post('/')

11.3 类型命名规范

类型用途命名格式示例
创建DTOCreateXxxDtoCreateUserDto
更新DTOUpdateXxxDtoUpdateUserDto
查询参数XxxQueryDtoUserQueryDto
实体模型XxxModelUserModel
前端展示XxxVoUserVo
数据库实体XxxEntityUserEntity

十二、工具和命令

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 从旧架构迁移步骤

  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
    
  2. 迁移数据库定义

    • 将现有的表定义移动到 xxx.schema.ts
    • 使用标准的Drizzle定义格式
  3. 创建Zod Schema

    • 使用 createInsertSchemacreateSelectSchema
    • 添加业务验证规则
  4. 更新Service层

    • 移除返回null的方法
    • 统一分页返回格式
    • 使用标准错误处理
  5. 更新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 全栈架构的实际重构经验总结,应根据项目发展持续更新和完善。