基于Hono、Supabase与Zod的API中数据库数据到TypeScript类型的映射方案优化咨询
首先可以明确说:你的核心思路完全没问题——把数据库返回的snake_case数据转换成API需要的camelCase格式,同时通过Zod Schema做校验和类型约束,这种数据库层与API层的格式隔离是非常合理的设计,能避免两端耦合,后续不管数据库结构还是API响应格式调整,都能独立修改,不用怀疑这个方向。
不过手动逐行映射字段确实有点繁琐,下面给你几个更高效、更贴合现有技术栈的优化方案,你可以根据自己的场景选择:
方案一:用Zod的transform整合映射与校验(最推荐,贴合现有技术栈)
既然已经在用Zod做Schema校验,完全可以把字段名转换逻辑直接整合到Schema里,不用在handler里手动写对象映射。
你可以先定义一个匹配Supabase返回格式的snake_case Schema,再通过Zod的transform方法把它转换成camelCase的结构,同时保留类型推断和校验能力:
// @schemas/sample.schema.ts import z from 'zod'; // 匹配Supabase返回的snake_case结构 const DbSampleSchema = z.object({ id: z.string().uuid(), sample_name: z.string(), other_property: z.string() }); // 转换为API需要的camelCase结构,同时导出类型 export const SampleSchema = DbSampleSchema.transform((dbData) => ({ id: dbData.id, sampleName: dbData.sample_name, otherProperty: dbData.other_property })); export type Sample = z.infer<typeof SampleSchema>;
然后在handler里,直接用SampleSchema.parse()处理Supabase返回的数据,自动完成字段映射+校验:
app.openapi(getSampleRoute, async (c) => { const { id: sampleId } = c.req.valid('param'); const supabase = getSupabase(c); const { data, error } = await supabase.from("samples") .select('*') .eq('id', sampleId) .limit(1) .single(); if (error) throw error; // 自动完成字段映射+校验 const sampleData = SampleSchema.parse(data); return c.json(sampleData, 200); });
这个方案的优势是:映射逻辑和校验逻辑集中在Schema里,字段修改时只需要改一处,符合单一职责原则,同时还能利用Zod的校验能力,避免脏数据流入API响应。
方案二:通用的snakeCase转camelCase工具函数(多表场景适用)
如果你的API有很多表需要做这种字段名转换,可以写一个通用的递归转换函数,一次性处理所有对象/数组的键名转换,不用每个Schema都写transform:
// 通用转换函数(可以放在utils里) function snakeToCamel(obj: unknown): unknown { if (Array.isArray(obj)) { return obj.map(snakeToCamel); } if (typeof obj === 'object' && obj !== null) { return Object.fromEntries( Object.entries(obj as Record<string, unknown>).map(([key, value]) => [ // 把snake_case转成camelCase,也可以自己实现替换逻辑 key.replace(/_([a-z])/g, (_, char) => char.toUpperCase()), snakeToCamel(value) ]) ); } return obj; }
然后在handler里用这个函数转换数据,再用Zod校验:
app.openapi(getSampleRoute, async (c) => { // ... 前面的查询逻辑不变 if (error) throw error; // 先转换键名,再用Schema校验 const camelCaseData = snakeToCamel(data) as Record<string, unknown>; const sampleData = SampleSchema.parse(camelCaseData); return c.json(sampleData, 200); });
这个方案适合需要批量转换的场景,减少重复代码,但要注意:如果有特殊字段(比如不是严格的snake_case转camelCase),需要在函数里加例外处理,或者结合Zod的transform覆盖特殊情况。
方案三:让Supabase直接返回camelCase(简单场景快选)
如果你的场景比较简单,字段不多,也可以直接在Supabase的查询语句里用别名,让数据库直接返回camelCase的字段:
const { data, error } = await supabase.from("samples") // 用as指定别名,直接返回camelCase .select(` id, sample_name as sampleName, other_property as otherProperty `) .eq('id', sampleId) .limit(1) .single();
这样返回的data直接就是符合Sample类型的结构,不用任何转换,直接返回即可。但这个方案的缺点是:查询语句和API响应格式耦合度较高,后续如果字段修改,需要同时修改SQL和Schema,适合小项目或字段少的场景。
总结
你的核心方向是对的,数据库与API的格式隔离非常有必要。具体选哪种方案:
- 优先选方案一:贴合现有Zod技术栈,逻辑集中,兼具校验和映射能力;
- 多表转换场景选方案二:通用函数减少重复代码;
- 简单小项目选方案三:最省事,不用写额外转换逻辑。
内容来源于stack exchange




