zod-openapi-share
is an extension package for @hono/zod-openapi
that lets you centralize and reuse response definitions across endpoints.
Normally, @hono/zod-openapi
requires you to redefine the same responses (e.g., error schemas) for every endpoint, but with zod-openapi-share
, you can avoid repetition and prevent definition drift, making your backend development using hono
+ @hono/zod-openapi
cleaner and more consistent.
Be sure to use the latest version.
zod-openapi-share
?In projects using hono
, you may have opportunities to use a convenient package called @hono/zod-openapi
as middleware for generating OpenAPI schemas.
This package allows you to define both OpenAPI schemas and Zod-based validation at the same time.
However, it has a major drawback: you must repeatedly write out the responses
definitions for every single status code across all endpoints.
In many cases, error responses share the exact same structure across endpoints — yet, even if they are identical, you still have to duplicate those definitions.
To solve this, zod-openapi-share
provides a way to centralize and reuse response definitions by wrapping around @hono/zod-openapi
.
Think of zod-openapi-share
as an extension package for @hono/zod-openapi
.
When using it, you’ll need three packages together: hono
, @hono/zod-openapi
, and zod-openapi-share
.
By unifying response definitions, you can develop without worrying about unintended inconsistencies between endpoints.
If you’re using hono and @hono/zod-openapi, be sure to try zod-openapi-share
!
hono
+ @hono/zod-openapi
)import { z, createRoute } from '@hono/zod-openapi';
// Commonly Used Response Schema
const ContentlyStatusCodeArray = [
100, 102, 103, 200, 201, 202, 203, 206, 207, 208, 226, 300, 301, 302, 303, 305, 306, 307, 308, 400, 401, 402, 403,
404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 421, 422, 423, 424, 425, 426, 428, 429,
431, 451, 500, 501, 502, 503, 504, 505, 506, 507, 508, 510, 511, -1,
] as const;
export const errorResponseSchema = z.object({
status: z.union(ContentlyStatusCodeArray.map((code) => z.literal(code))).meta({
example: 400,
description: 'HTTP Status Code',
}),
message: z.string().min(1).meta({
example: 'Bad Request',
description: 'Error Message',
}),
});
// Get Request Sample
const rootGetResponseBodySchema = z.object({
result: z.string().meta({
example: 'Hello, World!',
description: 'Root Endpoint Get Response',
}),
});
const rootGetRoute = createRoute({
path: '/',
method: 'get',
description: 'Sample Endpoint',
responses: {
200: {
description: 'OK',
content: {
'application/json': {
schema: rootGetResponseBodySchema,
},
},
},
//** Despite having the same definition, it must be defined repeatedly for each endpoint! */
// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> //
400: {
description: 'Bad Request',
content: { 'application/json': { schema: errorResponseSchema } },
},
500: {
description: 'Internal Server Error',
content: { 'application/json': { schema: errorResponseSchema } },
},
// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< //
},
});
// Post Request Sample
const rootPostRequestBodySchema = z.object({
input: z.string().min(1).max(100).meta({
example: 'Hello, World!',
description: 'Root Endpoint Post Request',
}),
});
const rootPostResponseBodySchema = z.object({
result: z.string().meta({
example: 'Hello, World!',
description: 'Root Endpoint Post Response',
}),
});
const rootPostRoute = createRoute({
path: '/',
method: 'post',
description: 'Sample Endpoint',
request: {
body: {
required: true,
content: {
'application/json': {
schema: rootPostRequestBodySchema,
},
},
},
},
responses: {
200: {
description: 'OK',
content: {
'application/json': {
schema: rootPostResponseBodySchema,
},
},
},
//** Despite having the same definition, it must be defined repeatedly for each endpoint! */
// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> //
400: {
description: 'Bad Request',
content: { 'application/json': { schema: errorResponseSchema } },
},
500: {
description: 'Internal Server Error',
content: { 'application/json': { schema: errorResponseSchema } },
},
// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< //
},
});
hono
+ @hono/zod-openapi
+ zod-openapi-share
)import { z } from '@hono/zod-openapi';
import { ZodOpenAPISchema } from 'zod-openapi-share';
// Commonly Used Response Schema
const ContentlyStatusCodeArray = [
100, 102, 103, 200, 201, 202, 203, 206, 207, 208, 226, 300, 301, 302, 303, 305, 306, 307, 308, 400, 401, 402, 403,
404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 421, 422, 423, 424, 425, 426, 428, 429,
431, 451, 500, 501, 502, 503, 504, 505, 506, 507, 508, 510, 511, -1,
] as const;
export const errorResponseSchema = z.object({
status: z.union(ContentlyStatusCodeArray.map((code) => z.literal(code))).meta({
example: 400,
description: 'HTTP Status Code',
}),
message: z.string().min(1).meta({
example: 'Bad Request',
description: 'Error Message',
}),
});
// Shared Responses Using ZodOpenAPISchema
const route = new ZodOpenAPISchema({
400: {
description: 'Bad Request',
content: { 'application/json': { schema: errorResponseSchema } },
},
500: {
description: 'Internal Server Error',
content: { 'application/json': { schema: errorResponseSchema } },
},
} as const);
// Get Request Sample
const rootGetResponseBodySchema = z.object({
result: z.string().meta({
example: 'Hello, World!',
description: 'Root Endpoint Get Response',
}),
});
const rootGetRoute = route.createSchema(
{
path: '/',
method: 'get',
description: 'Sample Endpoint',
responses: {
200: {
description: 'OK',
content: {
'application/json': {
schema: rootGetResponseBodySchema,
},
},
},
},
},
// You only need to describe the status codes of the response definitions shared in the array as the second argument!
[400, 500]
);
// Post Request Sample
const rootPostRequestBodySchema = z.object({
input: z.string().min(1).max(100).meta({
example: 'Hello, World!',
description: 'Root Endpoint Post Request',
}),
});
const rootPostResponseBodySchema = z.object({
result: z.string().meta({
example: 'Hello, World!',
description: 'Root Endpoint Post Response',
}),
});
const rootPostRoute = route.createSchema(
{
path: '/',
method: 'post',
description: 'Sample Endpoint',
request: {
body: {
required: true,
content: {
'application/json': {
schema: rootPostRequestBodySchema,
},
},
},
},
responses: {
200: {
description: 'OK',
content: {
'application/json': {
schema: rootPostResponseBodySchema,
},
},
},
},
}, // You only need to describe the status codes of the response definitions shared in the array as the second argument!
[400, 500]
);
Examples Here
cloudflare workers example
nodejs example
npm install hono @hono/zod-openapi zod-openapi-share
Zod
Schema and ZodOpenAPISchema
Class Instancehttps://github.com/Myxogastria0808/zod-openapi-share/tree/main/examples/nodejs/src/app/share.ts
import { z } from '@hono/zod-openapi';
import { ZodOpenAPISchema } from 'zod-openapi-share';
const ContentlyStatusCodeArray = [
100, 102, 103, 200, 201, 202, 203, 206, 207, 208, 226, 300, 301, 302, 303, 305, 306, 307, 308, 400, 401, 402, 403,
404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 421, 422, 423, 424, 425, 426, 428, 429,
431, 451, 500, 501, 502, 503, 504, 505, 506, 507, 508, 510, 511, -1,
] as const;
// Zod Schema for Error Response
export const errorResponseSchema = z.object({
status: z.union(ContentlyStatusCodeArray.map((code) => z.literal(code))).meta({
example: 400,
description: 'HTTP Status Code',
}),
message: z.string().min(1).meta({
example: 'Bad Request',
description: 'Error Message',
}),
});
export type ErrorResponseSchemaType = z.infer<typeof errorResponseSchema>;
// ZodOpenAPISchema Instance
/**
* Define Shared Responses Here
*/
export const route = new ZodOpenAPISchema({
400: {
description: 'Bad Request',
content: { 'application/json': { schema: errorResponseSchema } },
},
500: {
description: 'Internal Server Error',
content: { 'application/json': { schema: errorResponseSchema } },
},
} as const);
createSchema
MethodWhen you want to learn how to use createRoute
,
please refer to the @hono/zod-openapi.
zodOpenAPISchemaInstance.createSchema(
createRoute object (@hono/zod-openapi RouteConfig type object),
StatusCode[] (Array of status codes to be shared from ZodOpenAPISchema instance)
);
https://github.com/Myxogastria0808/zod-openapi-share/tree/main/examples/nodejs/src/app/route.ts
import { z } from '@hono/zod-openapi';
import { route } from './share.js';
const responseBodySchema = z.object({
result: z.string().meta({
example: 'Hello World!',
description: 'Sample Endpoint Response',
}),
});
export const rootRoute = route.createSchema(
{
path: '/',
method: 'get',
description: 'Sample Endpoint',
responses: {
200: {
description: 'OK',
content: {
'application/json': {
schema: responseBodySchema,
},
},
},
},
},
[400, 500]
);
root
Variable Into Hono app
Instancehttps://github.com/Myxogastria0808/zod-openapi-share/tree/main/examples/nodejs/src/app/index.ts
import { OpenAPIHono } from '@hono/zod-openapi';
import { cors } from 'hono/cors';
import { HTTPException } from 'hono/http-exception';
import { type ErrorResponseSchemaType } from './share.js';
import { rootRoute } from './route.js';
import { Scalar } from '@scalar/hono-api-reference';
export const api = () => {
const app = new OpenAPIHono({
// Zod Validation Error Hook
defaultHook: (result) => {
if (!result.success) {
console.error(result.error);
throw new HTTPException(400, {
message: 'Zod Validation Error',
});
}
},
});
// 404 Not Found Handler
app.notFound((c) => {
console.error(`Not Found: ${c.req.url}`);
return c.json({ status: 404, message: 'Not Found' } satisfies ErrorResponseSchemaType, 404);
});
// Other Error Handler
app.onError((error, c) => {
if (error instanceof HTTPException) {
return c.json(
{
status: error.status,
message: error.message,
} satisfies ErrorResponseSchemaType,
error.status
);
}
return c.json(
{
status: 500,
message: 'Internal Server Error',
} satisfies ErrorResponseSchemaType,
500
);
});
// Settings of CORS
app.use('*', cors());
// OpenAPI Document Endpoint
app.doc('/openapi', {
openapi: '3.1.0',
info: {
title: 'Echo API',
version: '1.0.0',
description: 'An example of OpenAPI with hono, @hono/zod-openapi, and zod-openapi-share.',
},
});
// Scalar Web UI Endpoint
// References
// https://guides.scalar.com/scalar/scalar-api-references/integrations/hono
app.get('/scalar', Scalar({ url: '/openapi' }));
/**
* Add route to app instance
*/
app.openapi(rootRoute, (c) => {
return c.json({ result: 'Hello World!' });
});
return app;
};
serve
(Define Entry Point)https://github.com/Myxogastria0808/zod-openapi-share/tree/main/examples/nodejs/src/index.ts
import { serve } from '@hono/node-server';
import { api } from './app/index.js';
serve(
{
fetch: api().fetch,
port: 3000,
},
(info) => {
console.log(`Server is running on http://localhost:${info.port}`);
}
);
https://github.com/Myxogastria0808/zod-openapi-share/tree/main/examples/nodejs/src/openapi.ts
import { api } from './app/index.js';
import fs from 'node:fs';
const docs = api().getOpenAPI31Document({
openapi: '3.1.0',
info: {
title: 'hono + @hono/zod-openapi + zod-openapi-share sample',
version: '1.0.0',
description: 'This is a sample project to generate OpenAPI documents with Hono and Zod.',
},
});
const json = JSON.stringify(docs, null, 2);
fs.writeFileSync('./openapi.json', json);
console.log(json);
scripts
to package.json{
"name": "example",
"type": "module",
"private": true,
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"openapi": "tsx src/openapi.ts" // Add this script for generating OpenAPI document
},
// [Omitted]
}
https://myxogastria0808.github.io/zod-openapi-share/
https://myxogastria0808.github.io/zod-openapi-share/coverage/
https://myxogastria0808.github.io/zod-openapi-share/html/
The accuracy of the contents of generated deepwiki has not been verified by me.
I recommend that you look at the documentation at typedoc.