Skip to content

Instantly share code, notes, and snippets.

@ayame113
Created October 6, 2025 01:53
Show Gist options
  • Select an option

  • Save ayame113/db158c55f289a09296d1009a307f9785 to your computer and use it in GitHub Desktop.

Select an option

Save ayame113/db158c55f289a09296d1009a307f9785 to your computer and use it in GitHub Desktop.
schema based hono example
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