03Full-stack TypeScript · Bun + Hono + React全栈 TypeScript · Bun + Hono + React
Jm2api
A video-generation API protocol adapter. Server and admin panel share one zod type contract; load balancing and an asset-mapping layer sit in between.一个视频生成 API 协议适配器。服务端和管理面板共用一份 zod 类型契约,中间是负载均衡和素材映射层。
// shared/src — imported by server and web
export const CreateTask = z.object({
model: z.string(),
content: z.array(ContentPart),
extra: Extra.optional(),
});
export type CreateTask = z.infer<typeof CreateTask>;01The problem问题
Two APIs, two shapes, one source of truth.两套 API,两种结构,一个事实来源。
Protocol translation协议转换
Clients speak one video-generation API format; the service behind it speaks another. Requests, task states and results all have to be mapped both ways.客户端讲一种视频生成 API 格式,后面的服务讲另一种。请求、任务状态和结果都要双向映射。
Types drift类型会漂移
When the server and the admin panel each define their own types, a renamed field breaks the panel at runtime, not at build time.服务端和管理面板各写各的类型时,改一个字段名,面板会在运行时才坏,而不是在构建时报错。
Long-running work长时间任务
Generation tasks run for minutes, so the service needs queuing, load balancing across upstream connections, and results that survive restarts.生成任务一跑就是几分钟,所以服务需要排队、在多个上游连接之间做负载均衡,以及重启后仍在的结果。
02What I built我做了什么
One contract, three packages.一份契约,三个包。
A protocol library and CLI, a Bun + Hono server, and a React Router + shadcn/ui admin panel, all in TypeScript. The zod schemas in shared/ are the only definition of every request and response.一个协议库和 CLI、一个 Bun + Hono 服务端、一个 React Router + shadcn/ui 管理面板,全部是 TypeScript。shared/ 里的 zod schema 是所有请求和响应的唯一定义。
- Validation at the edge: every request is parsed with the shared schema before it reaches business logic.在入口处校验:每个请求在进入业务逻辑前都先用共享 schema 解析。
- Load balancing across upstream connections, with health state and retries.在多个上游连接之间做负载均衡,带健康状态和重试。
- An asset-mapping layer that rewrites media references between the two formats and serves results from signed local URLs.素材映射层:在两种格式之间改写媒体引用,并通过签名的本地地址返回结果。
- Drizzle ORM on PostgreSQL with versioned migrations that run on start.PostgreSQL 上的 Drizzle ORM,带版本化迁移,启动时自动执行。
- No config files: every runtime setting lives in the panel; only the database URL and port come from the environment.没有配置文件:所有运行设置都在面板里,只有数据库地址和端口来自环境变量。
- Production hardening: the database listens on a Unix socket only, and the app binds to localhost behind a reverse proxy.生产加固:数据库只监听 Unix socket,应用只绑定 localhost,前面是反向代理。
03How it works怎么做的
A request, end to end.一个请求,从头到尾。
- STEP 01
Client request客户端请求
Standard API format, Bearer key.标准 API 格式,Bearer 密钥。
- STEP 02
Hono + zodHono + zod
Parsed against the shared contract.按共享契约解析。
- STEP 03
Balancer负载均衡
Picks a healthy upstream connection.选一条健康的上游连接。
- STEP 04
Protocol + assets协议 + 素材
Translates the task and maps media.转换任务,映射媒体。
- STEP 05
Task store任务存储
PostgreSQL; results via signed URLs.PostgreSQL;结果走签名地址。
05Tech used用到的技术
What it’s made of.用什么做的。
- Server服务端
- Bun
- Hono
- Drizzle ORM
- PostgreSQL
- Contract契约
- zod
- TypeScript
- shared/ packageshared/ 包
- Panel面板
- React Router
- shadcn/ui
- Vite
- Ops运维
- Docker Compose
- pm2
- Unix-socket databaseUnix socket 数据库
- Repo仓库
- Private repository私有仓库
- TypeScript 99%
06What I learned我学到了什么
Lessons I’ll keep.会一直记得的几件事。
- Make the compiler your reviewer. With one zod contract, a renamed field fails the build in both the server and the panel instead of failing a user.让编译器当审查员。只有一份 zod 契约时,改一个字段名会让服务端和面板一起构建失败,而不是让用户碰到错误。
- Settings belong where people can see them. Moving everything into the panel removed a class of “works on my machine” configuration bugs.设置应该放在看得见的地方。把所有设置都搬进面板,一类「在我机器上没问题」的配置问题就没有了。
- Security is mostly defaults. No TCP port for the database and localhost-only binding did more than any amount of clever code.安全大多来自默认值。数据库不开 TCP 端口、应用只绑定 localhost,比任何巧妙的代码都管用。