# Elysia 接口编写规范(Elysia Interface Development Standards) ## 目标 (Goal) 本规范旨在提供一套完整的 Elysia 接口开发标准,结合官方文档最佳实践、社区经验和项目实际需求,确保代码质量、类型安全和开发效率。 ## 核心原则 (Core Principles) ### 1. 一切皆组件 (Everything is a Component) - 每个 Elysia 实例都是一个组件 - 组件可以被插入到其他实例中 - 强制将应用拆分为小块,便于添加或移除功能 ### 2. 方法链式调用 (Method Chaining) - **必须始终使用方法链式调用** - 确保类型完整性和推断 - 每个方法返回新的类型引用 ### 3. 类型安全优先 (Type Safety First) - 使用 Elysia 内置类型系统 - 避免使用 `any` 类型 - 单一数据源原则 ## 项目结构规范 (Project Structure Standards) ### 推荐目录结构 ``` src/ ├── controllers/ # 控制器(路由与业务入口) │ ├── auth/ │ │ └── index.ts # 认证相关路由 │ └── user/ │ └── index.ts # 用户相关路由 ├── services/ # 业务逻辑服务层 │ ├── auth/ │ │ └── auth.service.ts │ └── user/ │ └── user.service.ts ├── validators/ # 参数校验(按路由结构组织) │ ├── global.response.ts │ ├── auth/ │ │ ├── auth.validator.ts │ │ └── auth.response.ts │ └── user/ │ ├── user.validator.ts │ └── user.response.ts ├── models/ # 数据模型 ├── plugins/ # Elysia 插件 ├── utils/ # 工具函数 ├── config/ # 配置文件 ├── type/ # 类型定义文件 └── app.ts # 应用入口 ``` ## 接口设计规范 (Interface Design Standards) ### 1. 控制器规范 (Controller Standards) **✅ 正确做法:使用 Elysia 实例作为控制器** ```typescript /** * @file 用户认证控制器 * @author 开发者姓名 * @date 2024-01-01 * @lastEditor 开发者姓名 * @lastEditTime 2024-01-01 * @description 用户认证相关接口,包含登录、注册、token 验证等功能 */ import { Elysia } from 'elysia'; import { jwtPlugin } from '@/plugins/jwt.plugins'; import { loginBodySchema, registerBodySchema, type LoginBody, type RegisterBody } from '@/validators/auth/auth.validator'; import { loginResponse200Schema, loginResponse400Schema, registerResponse200Schema, registerResponse400Schema } from '@/validators/auth/auth.response'; import { loginService, registerService } from '@/services/auth/auth.service'; /** * 认证控制器 * @description 处理用户认证相关的 HTTP 请求 */ export const authController = new Elysia({ prefix: '/api/auth' }) .use(jwtPlugin) .post('/login', ({ body, jwt, set }: { body: LoginBody; jwt: any; set: any }) => loginService(body, jwt, set), { body: loginBodySchema, detail: { tags: ['认证'], summary: '用户登录', description: '用户使用用户名和密码进行登录,成功后返回 JWT token', }, response: { 200: loginResponse200Schema, 400: loginResponse400Schema, }, } ) .post('/register', ({ body, set }: { body: RegisterBody; set: any }) => registerService(body, set), { body: registerBodySchema, detail: { tags: ['认证'], summary: '用户注册', description: '新用户注册账户', }, response: { 200: registerResponse200Schema, 400: registerResponse400Schema, }, } ); ``` **❌ 错误做法:创建单独的控制器类** ```typescript // ❌ 不要这样做 abstract class AuthController { static login(context: Context) { return AuthService.login(context.body); } } new Elysia() .post('/login', AuthController.login); ``` ### 2. 服务层规范 (Service Layer Standards) #### 非请求依赖服务 (Non-Request Dependent Service) ```typescript /** * @file 用户认证业务逻辑服务 * @author 开发者姓名 * @date 2024-01-01 * @lastEditor 开发者姓名 * @lastEditTime 2024-01-01 * @description 处理用户认证相关的业务逻辑,与 HTTP 请求解耦 */ import { hash, verify } from 'bun'; import { sign } from 'jsonwebtoken'; import type { LoginBody, RegisterBody } from '@/validators/auth/auth.validator'; /** * 认证服务类 * @description 处理用户认证业务逻辑 */ export abstract class AuthService { /** * 用户登录业务逻辑 * @param body 登录请求体 * @param jwt JWT 插件实例 * @param set Elysia set 对象 * @returns 登录响应 * @modification 张三 2024-01-02 添加密码验证逻辑 */ static async login( body: LoginBody, jwt: any, set: any ) { const { username, password } = body; try { // 查询用户 const user = await this.findUserByUsername(username); if (!user) { set.status = 400; return { code: 400, message: '用户名或密码错误', data: null, }; } // 验证密码 const isValidPassword = await this.verifyPassword(password, user.password); if (!isValidPassword) { set.status = 400; return { code: 400, message: '用户名或密码错误', data: null, }; } // 生成 token const token = await jwt.sign({ userId: user.id, username: user.username }); return { code: 0, message: '登录成功', data: { token, userInfo: { id: user.id, username: user.username, email: user.email } }, }; } catch (error) { set.status = 500; return { code: 500, message: '服务器内部错误', data: null, }; } } /** * 用户注册业务逻辑 * @param body 注册请求体 * @param set Elysia set 对象 * @returns 注册响应 */ static async register(body: RegisterBody, set: any) { const { username, email, password } = body; try { // 检查用户是否已存在 const existingUser = await this.findUserByUsername(username); if (existingUser) { set.status = 400; return { code: 400, message: '用户名已存在', data: null, }; } // 检查邮箱是否已存在 const existingEmail = await this.findUserByEmail(email); if (existingEmail) { set.status = 400; return { code: 400, message: '邮箱已被注册', data: null, }; } // 加密密码 const hashedPassword = await this.hashPassword(password); // 创建用户 const newUser = await this.createUser({ username, email, password: hashedPassword }); return { code: 0, message: '注册成功', data: { userId: newUser.id, username: newUser.username }, }; } catch (error) { set.status = 500; return { code: 500, message: '服务器内部错误', data: null, }; } } /** * 根据用户名查找用户 * @param username 用户名 * @returns 用户信息或 null */ private static async findUserByUsername(username: string) { // 实际项目中应该从数据库查询 // 这里仅作示例 return null; } /** * 根据邮箱查找用户 * @param email 邮箱 * @returns 用户信息或 null */ private static async findUserByEmail(email: string) { // 实际项目中应该从数据库查询 return null; } /** * 验证密码 * @param plainPassword 明文密码 * @param hashedPassword 加密后的密码 * @returns 是否匹配 */ private static async verifyPassword( plainPassword: string, hashedPassword: string ): Promise { return await verify(plainPassword, hashedPassword); } /** * 密码加密 * @param password 明文密码 * @returns 加密后的密码 */ private static async hashPassword(password: string): Promise { return await hash(password); } /** * 创建用户 * @param userData 用户数据 * @returns 创建的用户信息 */ private static async createUser(userData: { username: string; email: string; password: string; }) { // 实际项目中应该保存到数据库 return { id: Math.random().toString(36), ...userData }; } } ``` #### 请求依赖服务 (Request Dependent Service) ```typescript /** * 请求依赖的认证服务 * @description 需要访问请求上下文的服务应该作为 Elysia 实例 */ export const RequestAuthService = new Elysia({ name: 'Auth.Service' }) .derive({ as: 'global' }, ({ cookie: { session } }) => ({ Auth: { user: session.value } })) .macro(({ onBeforeHandle }) => ({ /** * 检查用户是否已登录 * @param value 是否需要登录 */ requireAuth(value: boolean) { if (value) { onBeforeHandle(({ Auth, status }) => { if (!Auth?.user) { return status(401, { code: 401, message: '请先登录', data: null }); } }); } } })); ``` ### 3. 参数校验规范 (Validation Standards) ```typescript /** * @file 认证接口参数校验规则 * @author 开发者姓名 * @date 2024-01-01 * @lastEditor 开发者姓名 * @lastEditTime 2024-01-01 * @description 认证相关接口的参数校验规则,包含详细的验证规则和错误提示 */ import { t } from 'elysia'; import type { Static } from 'elysia'; /** * 登录请求参数校验规则 * @property {string} username - 用户名,2-16位字符 * @property {string} password - 密码,6-32位字符 */ export const loginBodySchema = t.Object({ username: t.String({ minLength: 2, maxLength: 16, description: '用户名,2-16位字符', examples: ['admin', 'user123'] }), password: t.String({ minLength: 6, maxLength: 32, description: '密码,6-32位字符' }), }, { description: '用户登录请求参数' }); /** * 注册请求参数校验规则 * @property {string} username - 用户名,2-16位字符 * @property {string} email - 邮箱地址 * @property {string} password - 密码,6-32位字符 * @property {string} confirmPassword - 确认密码 */ export const registerBodySchema = t.Object({ username: t.String({ minLength: 2, maxLength: 16, description: '用户名,2-16位字符', pattern: '^[a-zA-Z0-9_]+$' // 只允许字母、数字、下划线 }), email: t.String({ format: 'email', description: '邮箱地址', examples: ['user@example.com'] }), password: t.String({ minLength: 6, maxLength: 32, description: '密码,6-32位字符,至少包含字母和数字', pattern: '^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d@$!%*#?&]+$' }), confirmPassword: t.String({ description: '确认密码,必须与密码一致' }) }, { description: '用户注册请求参数' }); /** * 密码重置请求参数校验规则 */ export const resetPasswordBodySchema = t.Object({ email: t.String({ format: 'email', description: '注册时使用的邮箱地址' }), newPassword: t.String({ minLength: 6, maxLength: 32, description: '新密码,6-32位字符' }), verificationCode: t.String({ minLength: 6, maxLength: 6, description: '6位数字验证码', pattern: '^\\d{6}$' }) }); /** * 查询参数校验规则 */ export const userListQuerySchema = t.Object({ page: t.Optional(t.Number({ minimum: 1, default: 1, description: '页码,从1开始' })), pageSize: t.Optional(t.Number({ minimum: 1, maximum: 100, default: 10, description: '每页数量,1-100' })), keyword: t.Optional(t.String({ maxLength: 50, description: '搜索关键词' })) }); /** * 路径参数校验规则 */ export const userParamsSchema = t.Object({ id: t.String({ minLength: 1, description: '用户ID' }) }); // 类型导出 export type LoginBody = Static; export type RegisterBody = Static; export type ResetPasswordBody = Static; export type UserListQuery = Static; export type UserParams = Static; ``` ### 4. 响应格式规范 (Response Format Standards) ```typescript /** * @file 认证接口响应格式定义 * @author 开发者姓名 * @date 2024-01-01 * @lastEditor 开发者姓名 * @lastEditTime 2024-01-01 * @description 认证相关接口的响应格式定义,确保响应结构的一致性 */ import { t } from 'elysia'; /** * 登录成功响应格式 */ export const loginResponse200Schema = t.Object({ code: t.Literal(0, { description: '成功响应码' }), message: t.String({ description: '响应消息', examples: ['登录成功'] }), data: t.Object({ /** JWT token */ token: t.String({ description: 'JWT 访问令牌', examples: ['eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'] }), /** 用户信息 */ userInfo: t.Object({ id: t.String({ description: '用户ID' }), username: t.String({ description: '用户名' }), email: t.String({ description: '邮箱地址' }) }) }) }, { description: '登录成功响应' }); /** * 登录失败响应格式 */ export const loginResponse400Schema = t.Object({ code: t.Literal(400, { description: '客户端错误响应码' }), message: t.String({ description: '错误消息', examples: ['用户名或密码错误', '参数验证失败'] }), data: t.Null({ description: '错误时数据为null' }), }, { description: '登录失败响应' }); /** * 注册成功响应格式 */ export const registerResponse200Schema = t.Object({ code: t.Literal(0), message: t.String({ examples: ['注册成功'] }), data: t.Object({ userId: t.String({ description: '新创建的用户ID' }), username: t.String({ description: '用户名' }) }) }); /** * 注册失败响应格式 */ export const registerResponse400Schema = t.Object({ code: t.Literal(400), message: t.String({ examples: ['用户名已存在', '邮箱已被注册', '密码不符合要求'] }), data: t.Null(), }); /** * 通用未授权响应格式 */ export const unauthorizedResponse401Schema = t.Object({ code: t.Literal(401), message: t.String({ examples: ['请先登录', 'Token已过期', 'Token无效'] }), data: t.Null(), }); /** * 用户列表响应格式 */ export const userListResponse200Schema = t.Object({ code: t.Literal(0), message: t.String(), data: t.Object({ list: t.Array(t.Object({ id: t.String(), username: t.String(), email: t.String(), createdAt: t.String({ format: 'date-time', description: '创建时间' }), updatedAt: t.String({ format: 'date-time', description: '更新时间' }) })), pagination: t.Object({ page: t.Number({ description: '当前页码' }), pageSize: t.Number({ description: '每页数量' }), total: t.Number({ description: '总条数' }), totalPages: t.Number({ description: '总页数' }) }) }) }); ``` ## 错误处理规范 (Error Handling Standards) ### 全局错误处理插件 ```typescript /** * @file 全局错误处理插件 * @author 开发者姓名 * @date 2024-01-01 * @description 统一处理应用中的错误,提供标准化的错误响应格式 */ import { Elysia } from 'elysia'; import { logger } from '@/utils/logger'; /** * 错误响应接口 */ interface ErrorResponse { code: number; message: string; data: null; } /** * 全局错误处理插件 */ export const errorHandlerPlugin = new Elysia({ name: 'errorHandler' }) .onError(({ code, error, set }) => { // 记录错误日志 logger.error('API Error:', { code, error: error.message, stack: error.stack, timestamp: new Date().toISOString() }); const response: ErrorResponse = { code: 500, message: '服务器内部错误', data: null }; switch (code) { case 'VALIDATION': set.status = 400; response.code = 400; response.message = '请求参数验证失败:' + error.message; break; case 'NOT_FOUND': set.status = 404; response.code = 404; response.message = '请求的资源不存在'; break; case 'PARSE': set.status = 400; response.code = 400; response.message = '请求数据格式错误'; break; case 'UNAUTHORIZED': set.status = 401; response.code = 401; response.message = '未授权访问'; break; case 'FORBIDDEN': set.status = 403; response.code = 403; response.message = '权限不足'; break; default: set.status = 500; response.code = 500; response.message = '服务器内部错误'; break; } return response; }); /** * 业务错误类 * @description 用于抛出业务逻辑错误 */ export class BusinessError extends Error { public code: number; constructor(code: number, message: string) { super(message); this.code = code; this.name = 'BusinessError'; } } /** * 抛出业务错误的辅助函数 * @param code 错误码 * @param message 错误消息 */ export function throwBusinessError(code: number, message: string): never { throw new BusinessError(code, message); } ``` ## 中间件与插件规范 (Middleware & Plugin Standards) ### JWT 认证插件 ```typescript /** * @file JWT 认证插件 * @author 开发者姓名 * @date 2024-01-01 * @description JWT 令牌处理插件,提供 token 生成和验证功能 */ import { Elysia } from 'elysia'; import { jwt } from '@elysiajs/jwt'; import { jwtConfig } from '@/config/jwt.config'; /** * JWT 认证插件 */ export const jwtPlugin = new Elysia({ name: 'jwt' }) .use(jwt({ name: 'jwt', secret: jwtConfig.secret, exp: jwtConfig.expiresIn })) .derive(({ jwt, headers }) => ({ /** * 获取当前用户信息 * @returns 用户信息或 null */ getCurrentUser: async () => { try { const authorization = headers.authorization; if (!authorization?.startsWith('Bearer ')) { return null; } const token = authorization.slice(7); const payload = await jwt.verify(token); return payload; } catch { return null; } } })) .macro(({ onBeforeHandle }) => ({ /** * 权限验证宏 * @param options 验证选项 */ auth(options: { required?: boolean } = {}) { const { required = true } = options; onBeforeHandle(async ({ getCurrentUser, status }) => { const user = await getCurrentUser(); if (required && !user) { return status(401, { code: 401, message: '请先登录', data: null }); } return { user }; }); } })); ``` ### 请求日志插件 ```typescript /** * @file 请求日志插件 * @author 开发者姓名 * @date 2024-01-01 * @description 记录 API 请求和响应的详细信息 */ import { Elysia } from 'elysia'; import { logger } from '@/utils/logger'; /** * 请求日志插件 */ export const requestLoggerPlugin = new Elysia({ name: 'requestLogger' }) .onRequest(({ request, path }) => { const startTime = Date.now(); logger.info('API Request', { method: request.method, url: request.url, path, userAgent: request.headers.get('user-agent'), ip: request.headers.get('x-forwarded-for') || 'unknown', timestamp: new Date().toISOString(), startTime }); // 将开始时间存储在请求上下文中 return { startTime }; }) .onAfterHandle(({ request, response, path, startTime }) => { const duration = Date.now() - (startTime || Date.now()); logger.info('API Response', { method: request.method, path, status: response.status, duration: `${duration}ms`, timestamp: new Date().toISOString() }); }); ``` ## 最佳实践示例 (Best Practice Examples) ### 完整的 CRUD 接口示例 ```typescript /** * @file 用户管理完整示例 * @author 开发者姓名 * @date 2024-01-01 * @description 展示完整的 CRUD 接口实现,包含分页、搜索、排序等功能 */ import { Elysia, t } from 'elysia'; import { jwtPlugin } from '@/plugins/jwt.plugins'; import { errorHandlerPlugin } from '@/plugins/errorHandler.plugins'; import { UserService } from '@/services/user/user.service'; // 参数校验 const createUserSchema = t.Object({ username: t.String({ minLength: 2, maxLength: 16 }), email: t.String({ format: 'email' }), password: t.String({ minLength: 6, maxLength: 32 }), role: t.Optional(t.Union([t.Literal('admin'), t.Literal('user')], { default: 'user' })) }); const updateUserSchema = t.Object({ username: t.Optional(t.String({ minLength: 2, maxLength: 16 })), email: t.Optional(t.String({ format: 'email' })), role: t.Optional(t.Union([t.Literal('admin'), t.Literal('user')])) }); const userParamsSchema = t.Object({ id: t.String({ minLength: 1 }) }); const userQuerySchema = t.Object({ page: t.Optional(t.Number({ minimum: 1, default: 1 })), pageSize: t.Optional(t.Number({ minimum: 1, maximum: 100, default: 10 })), keyword: t.Optional(t.String({ maxLength: 50 })), role: t.Optional(t.Union([t.Literal('admin'), t.Literal('user')])), sortBy: t.Optional(t.Union([t.Literal('createdAt'), t.Literal('username')], { default: 'createdAt' })), sortOrder: t.Optional(t.Union([t.Literal('asc'), t.Literal('desc')], { default: 'desc' })) }); // 响应格式 const userItemSchema = t.Object({ id: t.String(), username: t.String(), email: t.String(), role: t.String(), isActive: t.Boolean(), createdAt: t.String({ format: 'date-time' }), updatedAt: t.String({ format: 'date-time' }) }); const successResponse = (data: any) => t.Object({ code: t.Literal(0), message: t.String(), data }); const errorResponse = (code: number) => t.Object({ code: t.Literal(code), message: t.String(), data: t.Null() }); /** * 用户管理控制器 */ export const userController = new Elysia({ prefix: '/api/users' }) .use(jwtPlugin) .use(errorHandlerPlugin) // 获取用户列表(支持分页、搜索、排序) .get('/', async ({ query, getCurrentUser }) => { const currentUser = await getCurrentUser(); return await UserService.getUserList(query, currentUser); }, { query: userQuerySchema, detail: { tags: ['用户管理'], summary: '获取用户列表', description: '获取用户列表,支持分页、搜索和排序功能' }, response: { 200: successResponse(t.Object({ list: t.Array(userItemSchema), pagination: t.Object({ page: t.Number(), pageSize: t.Number(), total: t.Number(), totalPages: t.Number() }) })), 401: errorResponse(401) }, auth: { required: true } } ) // 获取单个用户 .get('/:id', async ({ params, getCurrentUser }) => { const currentUser = await getCurrentUser(); return await UserService.getUserById(params.id, currentUser); }, { params: userParamsSchema, detail: { tags: ['用户管理'], summary: '获取用户详情', description: '根据用户ID获取用户详细信息' }, response: { 200: successResponse(userItemSchema), 404: errorResponse(404), 401: errorResponse(401) }, auth: { required: true } } ) // 创建用户 .post('/', async ({ body, getCurrentUser }) => { const currentUser = await getCurrentUser(); return await UserService.createUser(body, currentUser); }, { body: createUserSchema, detail: { tags: ['用户管理'], summary: '创建用户', description: '创建新用户账户' }, response: { 200: successResponse(userItemSchema), 400: errorResponse(400), 401: errorResponse(401), 403: errorResponse(403) }, auth: { required: true } } ) // 更新用户 .put('/:id', async ({ params, body, getCurrentUser }) => { const currentUser = await getCurrentUser(); return await UserService.updateUser(params.id, body, currentUser); }, { params: userParamsSchema, body: updateUserSchema, detail: { tags: ['用户管理'], summary: '更新用户', description: '更新用户信息' }, response: { 200: successResponse(userItemSchema), 400: errorResponse(400), 404: errorResponse(404), 401: errorResponse(401), 403: errorResponse(403) }, auth: { required: true } } ) // 删除用户 .delete('/:id', async ({ params, getCurrentUser }) => { const currentUser = await getCurrentUser(); return await UserService.deleteUser(params.id, currentUser); }, { params: userParamsSchema, detail: { tags: ['用户管理'], summary: '删除用户', description: '删除指定用户' }, response: { 200: successResponse(t.Object({ deleted: t.Boolean() })), 404: errorResponse(404), 401: errorResponse(401), 403: errorResponse(403) }, auth: { required: true } } ); ``` --- **请严格遵守以上规范,确保 Elysia 接口的一致性、安全性和可维护性。**