Created
October 6, 2025 01:53
-
-
Save ayame113/db158c55f289a09296d1009a307f9785 to your computer and use it in GitHub Desktop.
schema based hono example
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import type { Context, Hono } from "hono"; | |
| import type { z } from "zod/v4"; | |
| const schema: unique symbol = Symbol("[[schema]]"); | |
| // for schema file | |
| interface DefineOperationOptions { | |
| method: "GET" | "POST"; | |
| path: string; | |
| input?: { | |
| param?: z.ZodType; | |
| query?: z.ZodType; | |
| body?: z.ZodType; | |
| }; | |
| output?: z.ZodType; | |
| } | |
| interface Operation<T extends DefineOperationOptions = DefineOperationOptions> { | |
| <U extends z.infer<T["input"]>>(input: U): { input: U; schema: T }; | |
| [schema]: T; | |
| } | |
| export function defineOperation<T extends DefineOperationOptions>(options: T): Operation<T> { | |
| return Object.assign( | |
| (input: z.infer<T["input"]>) => ({ input, schema: options }), | |
| { [schema]: options }, | |
| ) as Operation<T>; | |
| } | |
| // for client file | |
| interface DefineClientOptions { | |
| baseUrl: string; | |
| headers: z.ZodObject<{ [key: string]: z.ZodString }>; | |
| } | |
| export function defineClient<TOperation extends Operation>(baseClientOptions: DefineClientOptions) { | |
| return (clientOptions: { baseUrl?: string; headers: z.infer<typeof baseClientOptions["headers"]> }) => ({ | |
| async request(operation: ReturnType<TOperation>) { | |
| const baseUrl = clientOptions.baseUrl ?? baseClientOptions.baseUrl; | |
| const headers = clientOptions.headers; | |
| const url = new URL(operation.schema.path, baseUrl); | |
| const requestInit: RequestInit = { | |
| method: operation.schema.method, | |
| headers: { | |
| ...(operation.input | |
| ? { "Content-Type": "application/json" } | |
| : {}), | |
| ...headers, | |
| }, | |
| body: operation.input ? JSON.stringify(operation.input) : undefined, | |
| }; | |
| return await fetch(url.toString(), requestInit); | |
| }, | |
| }); | |
| } | |
| // for server file | |
| export function impl<TOperation extends Operation>(operation: TOperation) { | |
| return { | |
| for( | |
| app: Hono, | |
| handler: (args: { | |
| param: TOperation[typeof schema]["input"] extends { param: z.ZodType } | |
| ? z.infer<TOperation[typeof schema]["input"]["param"]> | |
| : never; | |
| query: TOperation[typeof schema]["input"] extends { query: z.ZodType } | |
| ? z.infer<TOperation[typeof schema]["input"]["query"]> | |
| : never; | |
| body: TOperation[typeof schema]["input"] extends { body: z.ZodType } | |
| ? z.infer<TOperation[typeof schema]["input"]["body"]> | |
| : never; | |
| }, c: Context) => Promise<z.infer<TOperation[typeof schema]["output"]>>, | |
| ) { | |
| const method = operation[schema].method.toLowerCase(); | |
| app.on(method, operation[schema].path, async (c) => { | |
| const param = operation[schema].input?.param?.safeParse(c.req.param()); | |
| if (param && !param.success) { | |
| return c.json({ error: param.error }, 400); | |
| } | |
| const query = operation[schema].input?.query?.safeParse(c.req.query()); | |
| if (query && !query.success) { | |
| return c.json({ error: query.error }, 400); | |
| } | |
| let body; | |
| try { | |
| const rawBody: unknown = await c.req.json(); | |
| body = operation[schema].input?.body?.safeParse(rawBody); | |
| if (body && !body.success) { | |
| return c.json({ error: body.error }, 400); | |
| } | |
| } catch { | |
| return c.json({ error: "Malformed JSON in request body" }, 400); | |
| } | |
| const input = { | |
| param: param?.data, | |
| query: query?.data, | |
| body: body?.data, | |
| } as Parameters<typeof handler>[0]; | |
| const result = await handler(input, c); | |
| if (!result) { | |
| return; | |
| } | |
| const output = operation[schema].output!.safeParse(result); | |
| if (!output.success) { | |
| return c.json({ error: output.error }, 500); | |
| } | |
| return c.json(output.data as Record<string, unknown>); | |
| }); | |
| }, | |
| }; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment