You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

基于Hono、Supabase与Zod的API中数据库数据到TypeScript类型的映射方案优化咨询

基于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

火山引擎 最新活动