Skip to content

Instantly share code, notes, and snippets.

@boristane
Created April 29, 2025 03:07
Show Gist options
  • Save boristane/a076454e980a8e538fae585212061042 to your computer and use it in GitHub Desktop.
Save boristane/a076454e980a8e538fae585212061042 to your computer and use it in GitHub Desktop.
Hono API
import { createRoute, OpenAPIHono } from "@hono/zod-openapi";
import { openapiGetResponses } from "./openapi";
import { z } from "@hono/zod-openapi";
const app = new OpenAPIHono()
const possibleStatusCodes = [200, 401, 500] as const satisfies readonly number[];
const route = createRoute({
operationId: "resource-name.list",
method: "get",
path: "/my-resource",
request: {},
responses: openapiGetResponses({
object: z.object({
key1: z.string(),
key2: z.string(),
}),
resourceName: "My resource",
codes: possibleStatusCodes,
}),
tags: ["My resource"],
summary: "Description of the resource",
});
export function list(router: typeof app) {
router.openapi(route, async (c, next) => {
return c.json({
message: { message: "Successful request" as const },
success: true as const,
error: null,
result: {
key1: "value1",
key2: "value2",
}
}, 200)
});
}
import { z } from "@hono/zod-openapi";
export const statusMap = {
200: { message: "Successful request" },
201: { message: "Resource created" },
202: { message: "Request accepted" },
400: { message: "Bad Request" },
401: { message: "Unauthorized" },
403: { message: "Forbidden" },
404: { message: "Not found" },
409: { message: "Conflict" },
429: { message: "Slow down" },
418: { message: "I'm a Teapot" },
500: { message: "Internal error" },
} as const;
export type AvailableResponseCodes = keyof typeof statusMap;
export type SuccessStatusCodes = 200 | 201 | 202;
export type ErrorStatusCodes = Exclude<AvailableResponseCodes, SuccessStatusCodes>;
export type StatusMessages = (typeof statusMap)[AvailableResponseCodes]["message"];
const baseSuccessSchema = <T extends z.ZodSchema, U extends SuccessStatusCodes>(object: T, status: U) =>
z.object({
message: z.object({
message: z.literal(statusMap[status].message) as z.ZodLiteral<(typeof statusMap)[U]["message"]>,
}),
success: z.literal(true),
error: z.null(),
result: object,
});
const baseErrorSchema = <T extends ErrorStatusCodes>(status: T) =>
z.object({
error: z.object({ message: z.literal(statusMap[status].message), detail: z.string().optional() }),
success: z.literal(false),
message: z.null(),
});
export const responseSchemas = {
200: <T extends z.ZodSchema>(object: T) =>
baseSuccessSchema(object, 200) satisfies z.ZodObject<{
message: z.ZodObject<{ message: z.ZodLiteral<"Successful request"> }>;
success: z.ZodLiteral<true>;
error: z.ZodNull;
result: T;
}>,
201: <T extends z.ZodSchema>(object: T) =>
baseSuccessSchema(object, 201) satisfies z.ZodObject<{
message: z.ZodObject<{ message: z.ZodLiteral<"Resource created"> }>;
success: z.ZodLiteral<true>;
error: z.ZodNull;
result: T;
}>,
202: baseSuccessSchema(z.any(), 202),
400: baseErrorSchema(400),
401: baseErrorSchema(401),
403: baseErrorSchema(403),
404: baseErrorSchema(404),
409: baseErrorSchema(409),
429: baseErrorSchema(429),
418: baseErrorSchema(418),
500: baseErrorSchema(500),
} as const;
type OpenApiHeader = {
type: "string";
example?: string;
description: string;
};
type OpenApiHeaders = Record<string, OpenApiHeader>;
type SchemaType<T extends AvailableResponseCodes, O extends z.ZodSchema> = T extends 200 | 201
? z.ZodObject<{
message: z.ZodObject<{ message: z.ZodLiteral<(typeof statusMap)[T]["message"]> }>;
success: z.ZodLiteral<true>;
error: z.ZodAny;
result: O;
}>
: (typeof responseSchemas)[T];
type ResponseSchemas<O extends z.ZodSchema> = {
[K in AvailableResponseCodes]: {
content: {
"application/json": {
schema: SchemaType<K, O>;
};
};
description: string;
headers?: OpenApiHeaders;
};
};
interface OpenApiResponseOptions<C extends AvailableResponseCodes, T extends z.ZodSchema> {
object: T;
resourceName: string;
codes: readonly C[];
headers?: {
codes: C[];
headers: OpenApiHeaders;
};
}
export function openapiGetResponses<C extends AvailableResponseCodes, T extends z.ZodSchema>({
object,
resourceName,
codes,
headers,
}: OpenApiResponseOptions<C, T>): { [K in C]: ResponseSchemas<T>[K] } {
const createResponseSchema = (code: C): ResponseSchemas<T>[C] => {
const schema = (() => {
if (code === 200) return responseSchemas[200](object);
if (code === 201) return responseSchemas[201](object);
return responseSchemas[code];
})();
const description = statusMap[code].message;
return {
content: {
"application/json": {
schema,
},
},
description,
} as ResponseSchemas<T>[C];
};
const result = {} as { [K in C]: ResponseSchemas<T>[K] };
for (const code of codes) {
result[code] = createResponseSchema(code);
if (headers?.codes.includes(code)) {
result[code].headers = headers.headers;
}
}
return result;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment