使用TypeScript和Kysely构建API时,如何优雅省略数据库响应字段?
问题:TypeScript + Kysely API如何安全返回用户数据?
我正在使用TypeScript和Kysely开发API,当前通过以下代码获取User模型并返回响应:
interface User { id: Generated<number> first_name: string last_name: string email: string password: string is_email_verified: boolean role: 'admin' | 'user' } interface Database { user: User } dbClient = dbClient || new Kysely<Database>({ dialect: new PlanetScaleDialect({url: config.mysql.url}) }) const getUserById = async (id: number) => { const user = await db .selectFrom('user') .selectAll() .where('user.id', '=', id) .executeTakeFirst() return user }
API返回逻辑如下:
const getUser: Handler<{ Bindings: Bindings }> = async (c) => { const { params } = userValidation.getUser.parse(c.req.parseBody()); const user = await userService.getUserById(params.userId); if (!user) { throw new ApiError(httpStatus.NOT_FOUND, 'User not found'); } return c.json(user, httpStatus.OK as StatusCode); };
但User模型包含password等非公开字段,直接返回会暴露敏感信息。我想到两种解决方案:
- 在
user.model.ts中定义UserResponse接口和转换函数,在API中调用该函数返回处理后的结果; - 创建
UserResponse类实现User接口,重写toJSON()方法移除敏感字段,需将Kysely返回结果转为自定义模型。
想请教哪种方案是更简洁、优雅的最优解?
方案分析与最优解
方案1:定义UserResponse接口+转换函数
具体实现:在user.model.ts中定义仅包含公开字段的UserResponse接口,再编写纯函数完成从User到UserResponse的转换:
// user.model.ts export interface UserResponse { id: number; first_name: string; last_name: string; email: string; is_email_verified: boolean; role: 'admin' | 'user'; } export const toUserResponse = (user: User): UserResponse => { // 解构排除敏感字段 const { password, ...publicFields } = user; return publicFields; };
API层调用转换函数:
const getUser: Handler<{ Bindings: Bindings }> = async (c) => { const { params } = userValidation.getUser.parse(c.req.parseBody()); const user = await userService.getUserById(params.userId); if (!user) { throw new ApiError(httpStatus.NOT_FOUND, 'User not found'); } return c.json(toUserResponse(user), httpStatus.OK as StatusCode); };
核心优势:
- 类型安全:TypeScript会严格校验转换前后的字段匹配,避免遗漏或错误
- 轻量高效:纯函数操作无额外实例化开销,性能最优
- 直观易维护:转换逻辑清晰直白,后续修改字段只需要调整接口和解构逻辑
- 适配Kysely:Kysely返回的是普通对象,直接解构即可完成转换,无需额外类型适配
方案2:UserResponse类+重写toJSON()
具体实现:创建类实现User接口,通过重写toJSON()方法过滤敏感字段,再将Kysely返回的对象转为类实例:
// user.model.ts export class UserResponse implements User { id: Generated<number>; first_name: string; last_name: string; email: string; password: string; is_email_verified: boolean; role: 'admin' | 'user'; constructor(user: User) { Object.assign(this, user); } toJSON() { const { password, ...publicFields } = this; return publicFields; } }
API层使用类实例:
const getUser: Handler<{ Bindings: Bindings }> = async (c) => { const { params } = userValidation.getUser.parse(c.req.parseBody()); const user = await userService.getUserById(params.userId); if (!user) { throw new ApiError(httpStatus.NOT_FOUND, 'User not found'); } return c.json(new UserResponse(user), httpStatus.OK as StatusCode); };
明显劣势:
- 冗余复杂:对于纯数据转换场景,类的实例化属于不必要的复杂度,违背"最小可用"原则
- 类型适配麻烦:
Generated<number>这类Kysely特有类型在类中处理容易出现类型冲突 - 性能开销:相比纯函数,类实例化会带来额外(虽小但无意义)的性能损耗
最优解:方案1
方案1是更简洁优雅的选择,原因如下:
- 符合TypeScript函数式编程风格,用纯函数处理数据映射更贴合场景
- 类型边界清晰,维护成本极低,后续扩展不同响应格式(比如管理员专属版、精简版)只需要新增接口和转换函数
- 完全适配Kysely的返回值特性,无需额外类型转换步骤
额外优化建议:可以把转换逻辑下沉到Service层,让API层只负责响应返回,职责更清晰:
const getUserById = async (id: number): Promise<UserResponse | undefined> => { const user = await db .selectFrom('user') .selectAll() .where('user.id', '=', id) .executeTakeFirst(); return user ? toUserResponse(user) : undefined; };
内容的提问来源于stack exchange,提问作者OultimoCoder
相关产品推荐
相关产品推荐

