Created
April 29, 2025 03:07
-
-
Save boristane/a076454e980a8e538fae585212061042 to your computer and use it in GitHub Desktop.
Hono API
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 { 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) | |
}); | |
} | |
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 { 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