zod-openapi-share 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Yuki Osada
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,537 @@
1
+ # zod-openapi-share
2
+
3
+ [![Test](https://github.com/Myxogastria0808/zod-openapi-share/actions/workflows/test.yaml/badge.svg)](https://github.com/Myxogastria0808/zod-openapi-share/actions/workflows/test.yaml)
4
+ [![Docs](https://github.com/Myxogastria0808/zod-openapi-share/actions/workflows/docs.yaml/badge.svg)](https://github.com/Myxogastria0808/zod-openapi-share/actions/workflows/docs.yaml)
5
+ [![NPM Version](https://img.shields.io/npm/v/zod-openapi-share.svg)](https://www.npmjs.com/package/zod-openapi-share)
6
+ ![GitHub Release](https://img.shields.io/github/v/release/Myxogastria0808/zod-openapi-share)
7
+ ![NPM Type Definitions](https://img.shields.io/npm/types/zod-openapi-share)
8
+ [![Download NPM](https://img.shields.io/npm/dm/zod-openapi-share.svg?style=flat)](https://www.npmjs.com/package/zod-openapi-share)
9
+ ![GitHub License](https://img.shields.io/github/license/Myxogastria0808/zod-openapi-share)
10
+ ![Vitest](https://img.shields.io/badge/-vitest-6e9f18?style=flat&logo=vitest&logoColor=ffffff)
11
+ ![Typedoc](https://img.shields.io/badge/docs-typedoc-blue?style=flat-square&logo=typescript&logoColor=white)
12
+ [![RenovateBot](https://img.shields.io/badge/RenovateBot-1A1F6C?logo=renovate&logoColor=fff)](#)
13
+
14
+ `zod-openapi-share` is an extension for `@hono/zod-openapi` that lets you centralize and reuse response definitions across endpoints.
15
+ 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.
16
+
17
+ > [!IMPORTANT]
18
+ > Be sure to use the latest version.
19
+
20
+ ## What is `zod-openapi-share`?
21
+
22
+ In projects using `hono`, you may have opportunities to use a convenient package called `@hono/zod-openapi` as middleware for generating OpenAPI schemas.
23
+ This package allows you to define both **OpenAPI schemas** and **Zod-based validation** at the same time.
24
+
25
+ However, it has a major drawback: you must repeatedly write out the `responses` definitions for every single status code across all endpoints.
26
+ 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.
27
+
28
+ To solve this, `zod-openapi-share` provides a way to **centralize and reuse response definitions** by wrapping around `@hono/zod-openapi`.
29
+ Think of `zod-openapi-share` as an extension package for `@hono/zod-openapi`.
30
+ When using it, you’ll need three packages together: `hono`, `@hono/zod-openapi`, and `zod-openapi-share`.
31
+
32
+ By unifying response definitions, you can develop without worrying about unintended inconsistencies between endpoints.
33
+ If you’re using hono and @hono/zod-openapi, be sure to try **zod-openapi-share**!
34
+
35
+ ### before (`hono` + `@hono/zod-openapi`)
36
+
37
+ ```typescript
38
+ import { z, createRoute } from '@hono/zod-openapi';
39
+
40
+ // Commonly Used Response Schema
41
+ const ContentlyStatusCodeArray = [
42
+ 100, 102, 103, 200, 201, 202, 203, 206, 207, 208, 226, 300, 301, 302, 303, 305, 306, 307, 308, 400, 401, 402, 403,
43
+ 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 421, 422, 423, 424, 425, 426, 428, 429,
44
+ 431, 451, 500, 501, 502, 503, 504, 505, 506, 507, 508, 510, 511, -1,
45
+ ] as const;
46
+
47
+ export const errorResponseSchema = z.object({
48
+ status: z.union(ContentlyStatusCodeArray.map((code) => z.literal(code))).meta({
49
+ example: 400,
50
+ description: 'HTTP Status Code',
51
+ }),
52
+ message: z.string().min(1).meta({
53
+ example: 'Bad Request',
54
+ description: 'Error Message',
55
+ }),
56
+ });
57
+
58
+ // Get Request Sample
59
+ const rootGetResponseBodySchema = z.object({
60
+ result: z.string().meta({
61
+ example: 'Hello, World!',
62
+ description: 'Root Endpoint Get Response',
63
+ }),
64
+ });
65
+
66
+ const rootGetRoute = createRoute({
67
+ path: '/',
68
+ method: 'get',
69
+ description: 'Sample Endpoint',
70
+ responses: {
71
+ 200: {
72
+ description: 'OK',
73
+ content: {
74
+ 'application/json': {
75
+ schema: rootGetResponseBodySchema,
76
+ },
77
+ },
78
+ },
79
+ //** Despite having the same definition, it must be defined repeatedly for each endpoint! */
80
+ // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> //
81
+ 400: {
82
+ description: 'Bad Request',
83
+ content: { 'application/json': { schema: errorResponseSchema } },
84
+ },
85
+ 500: {
86
+ description: 'Internal Server Error',
87
+ content: { 'application/json': { schema: errorResponseSchema } },
88
+ },
89
+ // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< //
90
+ },
91
+ });
92
+
93
+ // Post Request Sample
94
+ const rootPostRequestBodySchema = z.object({
95
+ input: z.string().min(1).max(100).meta({
96
+ example: 'Hello, World!',
97
+ description: 'Root Endpoint Post Request',
98
+ }),
99
+ });
100
+
101
+ const rootPostResponseBodySchema = z.object({
102
+ result: z.string().meta({
103
+ example: 'Hello, World!',
104
+ description: 'Root Endpoint Post Response',
105
+ }),
106
+ });
107
+
108
+ const rootPostRoute = createRoute({
109
+ path: '/',
110
+ method: 'post',
111
+ description: 'Sample Endpoint',
112
+ request: {
113
+ body: {
114
+ required: true,
115
+ content: {
116
+ 'application/json': {
117
+ schema: rootPostRequestBodySchema,
118
+ },
119
+ },
120
+ },
121
+ },
122
+ responses: {
123
+ 200: {
124
+ description: 'OK',
125
+ content: {
126
+ 'application/json': {
127
+ schema: rootPostResponseBodySchema,
128
+ },
129
+ },
130
+ },
131
+ //** Despite having the same definition, it must be defined repeatedly for each endpoint! */
132
+ // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> //
133
+ 400: {
134
+ description: 'Bad Request',
135
+ content: { 'application/json': { schema: errorResponseSchema } },
136
+ },
137
+ 500: {
138
+ description: 'Internal Server Error',
139
+ content: { 'application/json': { schema: errorResponseSchema } },
140
+ },
141
+ // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< //
142
+ },
143
+ });
144
+
145
+ ```
146
+
147
+ ### after (`hono` + `@hono/zod-openapi` + `zod-openapi-share`)
148
+
149
+ ```typescript
150
+ import { z } from '@hono/zod-openapi';
151
+ import { ZodOpenAPISchema } from 'zod-openapi-share';
152
+
153
+ // Commonly Used Response Schema
154
+ const ContentlyStatusCodeArray = [
155
+ 100, 102, 103, 200, 201, 202, 203, 206, 207, 208, 226, 300, 301, 302, 303, 305, 306, 307, 308, 400, 401, 402, 403,
156
+ 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 421, 422, 423, 424, 425, 426, 428, 429,
157
+ 431, 451, 500, 501, 502, 503, 504, 505, 506, 507, 508, 510, 511, -1,
158
+ ] as const;
159
+
160
+ export const errorResponseSchema = z.object({
161
+ status: z.union(ContentlyStatusCodeArray.map((code) => z.literal(code))).meta({
162
+ example: 400,
163
+ description: 'HTTP Status Code',
164
+ }),
165
+ message: z.string().min(1).meta({
166
+ example: 'Bad Request',
167
+ description: 'Error Message',
168
+ }),
169
+ });
170
+
171
+ // Shared Responses Using ZodOpenAPISchema
172
+ const route = new ZodOpenAPISchema({
173
+ 400: {
174
+ description: 'Bad Request',
175
+ content: { 'application/json': { schema: errorResponseSchema } },
176
+ },
177
+ 500: {
178
+ description: 'Internal Server Error',
179
+ content: { 'application/json': { schema: errorResponseSchema } },
180
+ },
181
+ } as const);
182
+
183
+ // Get Request Sample
184
+ const rootGetResponseBodySchema = z.object({
185
+ result: z.string().meta({
186
+ example: 'Hello, World!',
187
+ description: 'Root Endpoint Get Response',
188
+ }),
189
+ });
190
+
191
+ const rootGetRoute = route.createSchema(
192
+ {
193
+ path: '/',
194
+ method: 'get',
195
+ description: 'Sample Endpoint',
196
+ responses: {
197
+ 200: {
198
+ description: 'OK',
199
+ content: {
200
+ 'application/json': {
201
+ schema: rootGetResponseBodySchema,
202
+ },
203
+ },
204
+ },
205
+ },
206
+ },
207
+ // You only need to describe the status codes of the response definitions shared in the array as the second argument!
208
+ [400, 500]
209
+ );
210
+
211
+ // Post Request Sample
212
+ const rootPostRequestBodySchema = z.object({
213
+ input: z.string().min(1).max(100).meta({
214
+ example: 'Hello, World!',
215
+ description: 'Root Endpoint Post Request',
216
+ }),
217
+ });
218
+
219
+ const rootPostResponseBodySchema = z.object({
220
+ result: z.string().meta({
221
+ example: 'Hello, World!',
222
+ description: 'Root Endpoint Post Response',
223
+ }),
224
+ });
225
+
226
+ const rootPostRoute = route.createSchema(
227
+ {
228
+ path: '/',
229
+ method: 'post',
230
+ description: 'Sample Endpoint',
231
+ request: {
232
+ body: {
233
+ required: true,
234
+ content: {
235
+ 'application/json': {
236
+ schema: rootPostRequestBodySchema,
237
+ },
238
+ },
239
+ },
240
+ },
241
+ responses: {
242
+ 200: {
243
+ description: 'OK',
244
+ content: {
245
+ 'application/json': {
246
+ schema: rootPostResponseBodySchema,
247
+ },
248
+ },
249
+ },
250
+ },
251
+ }, // You only need to describe the status codes of the response definitions shared in the array as the second argument!
252
+ [400, 500]
253
+ );
254
+ ```
255
+
256
+ ## How to Use
257
+
258
+ **Examples Here**
259
+
260
+ - cloudflare workers example
261
+ - https://github.com/Myxogastria0808/zod-openapi-share/tree/main/examples/cloudflare-workers/
262
+
263
+ - nodejs example
264
+ - https://github.com/Myxogastria0808/zod-openapi-share/tree/main/examples/nodejs/
265
+
266
+ 1. Install Packages
267
+
268
+ ```sh
269
+ npm install hono @hono/zod-openapi zod-openapi-share
270
+ ```
271
+
272
+ 2. Create `Zod` Schema and `ZodOpenAPISchema` Class Instance
273
+
274
+ - Example
275
+
276
+ https://github.com/Myxogastria0808/zod-openapi-share/tree/main/examples/nodejs/src/app/share.ts
277
+
278
+ ```typescript
279
+ import { z } from '@hono/zod-openapi';
280
+ import { ZodOpenAPISchema } from 'zod-openapi-share';
281
+
282
+ const ContentlyStatusCodeArray = [
283
+ 100, 102, 103, 200, 201, 202, 203, 206, 207, 208, 226, 300, 301, 302, 303, 305, 306, 307, 308, 400, 401, 402, 403,
284
+ 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 421, 422, 423, 424, 425, 426, 428, 429,
285
+ 431, 451, 500, 501, 502, 503, 504, 505, 506, 507, 508, 510, 511, -1,
286
+ ] as const;
287
+
288
+ // Zod Schema for Error Response
289
+ export const errorResponseSchema = z.object({
290
+ status: z.union(ContentlyStatusCodeArray.map((code) => z.literal(code))).meta({
291
+ example: 400,
292
+ description: 'HTTP Status Code',
293
+ }),
294
+ message: z.string().min(1).meta({
295
+ example: 'Bad Request',
296
+ description: 'Error Message',
297
+ }),
298
+ });
299
+
300
+ export type ErrorResponseSchemaType = z.infer<typeof errorResponseSchema>;
301
+
302
+ // ZodOpenAPISchema Instance
303
+ /**
304
+ * Define Shared Responses Here
305
+ */
306
+ export const route = new ZodOpenAPISchema({
307
+ 400: {
308
+ description: 'Bad Request',
309
+ content: { 'application/json': { schema: errorResponseSchema } },
310
+ },
311
+ 500: {
312
+ description: 'Internal Server Error',
313
+ content: { 'application/json': { schema: errorResponseSchema } },
314
+ },
315
+ } as const);
316
+
317
+ ```
318
+
319
+ 3. Create RouteConfig Type Object
320
+
321
+ - `createSchema` Method
322
+
323
+ When you want to learn how to use `createRoute`,
324
+ please refer to the [@hono/zod-openapi](https://hono.dev/examples/zod-openapi).
325
+
326
+ ```ts
327
+ zodOpenAPISchemaInstance.createSchema(
328
+ createRoute object (@hono/zod-openapi RouteConfig type object),
329
+ StatusCode[] (Array of status codes to be shared from ZodOpenAPISchema instance)
330
+ );
331
+ ```
332
+
333
+ - Example
334
+
335
+ https://github.com/Myxogastria0808/zod-openapi-share/tree/main/examples/nodejs/src/app/route.ts
336
+
337
+ ```typescript
338
+ import { z } from '@hono/zod-openapi';
339
+ import { route } from './share.js';
340
+
341
+ const responseBodySchema = z.object({
342
+ result: z.string().meta({
343
+ example: 'Hello World!',
344
+ description: 'Sample Endpoint Response',
345
+ }),
346
+ });
347
+
348
+ export const rootRoute = route.createSchema(
349
+ {
350
+ path: '/',
351
+ method: 'get',
352
+ description: 'Sample Endpoint',
353
+ responses: {
354
+ 200: {
355
+ description: 'OK',
356
+ content: {
357
+ 'application/json': {
358
+ schema: responseBodySchema,
359
+ },
360
+ },
361
+ },
362
+ },
363
+ },
364
+ [400, 500]
365
+ );
366
+
367
+ ```
368
+
369
+ 4. Insert `root` Variable Into Hono `app` Instance
370
+
371
+ - Example
372
+
373
+ https://github.com/Myxogastria0808/zod-openapi-share/tree/main/examples/nodejs/src/app/index.ts
374
+
375
+ ```typescript
376
+ import { OpenAPIHono } from '@hono/zod-openapi';
377
+ import { cors } from 'hono/cors';
378
+ import { HTTPException } from 'hono/http-exception';
379
+ import { type ErrorResponseSchemaType } from './share.js';
380
+ import { rootRoute } from './route.js';
381
+ import { Scalar } from '@scalar/hono-api-reference';
382
+
383
+ export const api = () => {
384
+ const app = new OpenAPIHono({
385
+ // Zod Validation Error Hook
386
+ defaultHook: (result) => {
387
+ if (!result.success) {
388
+ console.error(result.error);
389
+ throw new HTTPException(400, {
390
+ message: 'Zod Validation Error',
391
+ });
392
+ }
393
+ },
394
+ });
395
+
396
+ // 404 Not Found Handler
397
+ app.notFound((c) => {
398
+ console.error(`Not Found: ${c.req.url}`);
399
+ return c.json({ status: 404, message: 'Not Found' } satisfies ErrorResponseSchemaType, 404);
400
+ });
401
+ // Other Error Handler
402
+ app.onError((error, c) => {
403
+ if (error instanceof HTTPException) {
404
+ return c.json(
405
+ {
406
+ status: error.status,
407
+ message: error.message,
408
+ } satisfies ErrorResponseSchemaType,
409
+ error.status
410
+ );
411
+ }
412
+ return c.json(
413
+ {
414
+ status: 500,
415
+ message: 'Internal Server Error',
416
+ } satisfies ErrorResponseSchemaType,
417
+ 500
418
+ );
419
+ });
420
+
421
+ // Settings of CORS
422
+ app.use('*', cors());
423
+
424
+ // OpenAPI Document Endpoint
425
+ app.doc('/openapi', {
426
+ openapi: '3.0.0',
427
+ info: {
428
+ title: 'Echo API',
429
+ version: '1.0.0',
430
+ description: '受け取った入力値をそのまま応答するAPI',
431
+ },
432
+ });
433
+
434
+ // Scalar Web UI Endpoint
435
+ // References
436
+ // https://guides.scalar.com/scalar/scalar-api-references/integrations/hono
437
+ app.get('/scalar', Scalar({ url: '/openapi' }));
438
+
439
+ /**
440
+ * Add route to app instance
441
+ */
442
+ app.openapi(rootRoute, (c) => {
443
+ return c.json({ result: 'Hello World!' });
444
+ });
445
+
446
+ return app;
447
+ };
448
+
449
+ ```
450
+
451
+ 5. Define `serve` (Define Entry Point)
452
+
453
+ - Example
454
+
455
+ https://github.com/Myxogastria0808/zod-openapi-share/tree/main/examples/nodejs/src/index.ts
456
+
457
+ ```ts
458
+ import { serve } from '@hono/node-server';
459
+ import { api } from './app/index.js';
460
+
461
+ serve(
462
+ {
463
+ fetch: api().fetch,
464
+ port: 3000,
465
+ },
466
+ (info) => {
467
+ console.log(`Server is running on http://localhost:${info.port}`);
468
+ }
469
+ );
470
+
471
+ ```
472
+
473
+ 6. Define Generate OpenAPI Document Program
474
+
475
+ - Example
476
+
477
+ https://github.com/Myxogastria0808/zod-openapi-share/tree/main/examples/nodejs/src/openapi.ts
478
+
479
+ ```ts
480
+ import { api } from './app/index.js';
481
+ import fs from 'node:fs';
482
+
483
+ const docs = api().getOpenAPIDocument({
484
+ openapi: '3.0.0',
485
+ info: {
486
+ title: 'hono + @hono/zod-openapi + zod-openapi-share sample',
487
+ version: '1.0.0',
488
+ description: 'This is a sample project to generate OpenAPI documents with Hono and Zod.',
489
+ },
490
+ });
491
+
492
+ const json = JSON.stringify(docs, null, 2);
493
+
494
+ fs.writeFileSync('./openapi.json', json);
495
+ console.log(json);
496
+
497
+ ```
498
+
499
+ 7. Add generate openapi.json `scripts` to package.json
500
+
501
+ - Example
502
+
503
+ ```json
504
+ {
505
+ "name": "example",
506
+ "type": "module",
507
+ "private": true,
508
+ "scripts": {
509
+ "dev": "tsx watch src/index.ts",
510
+ "build": "tsc",
511
+ "start": "node dist/index.js",
512
+ "openapi": "tsx src/openapi.ts" // Add this script for generating OpenAPI document
513
+ },
514
+ // [Omitted]
515
+ }
516
+ ```
517
+
518
+ ## HTML Documentation Generated by typedoc
519
+
520
+ https://myxogastria0808.github.io/zod-openapi-share/
521
+
522
+ ## Test Coverage Generated by @vitest/coverage-v8
523
+
524
+ https://myxogastria0808.github.io/zod-openapi-share/coverage/
525
+
526
+ ## Test Result Generated by @vitest/ui
527
+
528
+ https://myxogastria0808.github.io/zod-openapi-share/html/
529
+
530
+ ## DeepWiki
531
+
532
+ > [!WARNING]
533
+ > The accuracy of the contents of generated deepwiki has not been verified by me.
534
+ >
535
+ > I recommend that you look at the documentation at [typedoc](https://myxogastria0808.github.io/zod-openapi-share/).
536
+
537
+ https://deepwiki.com/Myxogastria0808/zod-openapi-share/