@hono/zod-openapi 0.0.1 → 0.1.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/README.md +124 -29
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +13 -3
- package/dist/index.d.ts +13 -3
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,22 +1,29 @@
|
|
|
1
1
|
# Zod OpenAPI Hono
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
This is based on [Zod to OpenAPI](https://github.com/asteasolutions/zod-to-openapi).
|
|
5
|
-
For details on creating schemas and defining routes, please refer to this resource.
|
|
3
|
+
**Zod OpenAPI Hono** is an extended Hono class that supports OpenAPI. With it, you can validate values and types using [**Zod**](https://zod.dev/) and generate OpenAPI Swagger documentation. This is based on [**Zod to OpenAPI**](https://github.com/asteasolutions/zod-to-openapi) (thanks a lot!). For details on creating schemas and defining routes, please refer to [the "Zod to OpenAPI" resource](https://github.com/asteasolutions/zod-to-openapi).
|
|
6
4
|
|
|
7
|
-
|
|
5
|
+
_Note: This is not standalone middleware but is hosted on the monorepo "[github.com/honojs/middleware](https://github.com/honojs/middleware)"._
|
|
6
|
+
|
|
7
|
+
## Limitations
|
|
8
|
+
|
|
9
|
+
- Currently, it does not support validation of _headers_ and _cookies_.
|
|
10
|
+
- An instance of Zod OpenAPI Hono cannot be used as a "subApp" in conjunction with `rootApp.route('/api', subApp)`.
|
|
8
11
|
|
|
9
12
|
## Usage
|
|
10
13
|
|
|
11
|
-
###
|
|
14
|
+
### Installation
|
|
12
15
|
|
|
13
|
-
|
|
16
|
+
You can install it via npm. It should be installed alongside `hono` and `zod`.
|
|
17
|
+
|
|
18
|
+
```sh
|
|
14
19
|
npm i hono zod @hono/zod-openapi
|
|
15
20
|
```
|
|
16
21
|
|
|
17
|
-
###
|
|
22
|
+
### Basic Usage
|
|
23
|
+
|
|
24
|
+
#### Setting up your application
|
|
18
25
|
|
|
19
|
-
|
|
26
|
+
First, define your schemas with Zod. The `z` object should be imported from `@hono/zod-openapi`:
|
|
20
27
|
|
|
21
28
|
```ts
|
|
22
29
|
import { z } from '@hono/zod-openapi'
|
|
@@ -36,8 +43,8 @@ const ParamsSchema = z.object({
|
|
|
36
43
|
|
|
37
44
|
const UserSchema = z
|
|
38
45
|
.object({
|
|
39
|
-
id: z.
|
|
40
|
-
example: 123,
|
|
46
|
+
id: z.string().openapi({
|
|
47
|
+
example: '123',
|
|
41
48
|
}),
|
|
42
49
|
name: z.string().openapi({
|
|
43
50
|
example: 'John Doe',
|
|
@@ -49,7 +56,7 @@ const UserSchema = z
|
|
|
49
56
|
.openapi('User')
|
|
50
57
|
```
|
|
51
58
|
|
|
52
|
-
|
|
59
|
+
Next, create a route:
|
|
53
60
|
|
|
54
61
|
```ts
|
|
55
62
|
import { createRoute } from '@hono/zod-openapi'
|
|
@@ -67,27 +74,86 @@ const route = createRoute({
|
|
|
67
74
|
schema: UserSchema,
|
|
68
75
|
},
|
|
69
76
|
},
|
|
70
|
-
description: '
|
|
77
|
+
description: 'Retrieve the user',
|
|
71
78
|
},
|
|
79
|
+
},
|
|
80
|
+
})
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Finally, set up the app:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import { OpenAPIHono } from '@hono/zod-openapi'
|
|
87
|
+
|
|
88
|
+
const app = new OpenAPIHono()
|
|
89
|
+
|
|
90
|
+
app.openapi(route, (c) => {
|
|
91
|
+
const { id } = c.req.valid('param')
|
|
92
|
+
return c.jsonT({
|
|
93
|
+
id,
|
|
94
|
+
age: 20,
|
|
95
|
+
name: 'Ultra-man',
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
// The OpenAPI documentation will be available at /doc
|
|
100
|
+
app.doc('/doc', {
|
|
101
|
+
openapi: '3.0.0',
|
|
102
|
+
info: {
|
|
103
|
+
version: '1.0.0',
|
|
104
|
+
title: 'My API',
|
|
105
|
+
},
|
|
106
|
+
})
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
You can start your app just like you would with Hono. For Cloudflare Workers and Bun, use this entry point:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
export default app
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Handling Validation Errors
|
|
116
|
+
|
|
117
|
+
Validation errors can be handled as follows:
|
|
118
|
+
|
|
119
|
+
First, define the error schema:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
const ErrorSchema = z.object({
|
|
123
|
+
code: z.number().openapi({
|
|
124
|
+
example: 400,
|
|
125
|
+
}),
|
|
126
|
+
message: z.string().openapi({
|
|
127
|
+
example: 'Bad Request',
|
|
128
|
+
}),
|
|
129
|
+
})
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Then, add the error response:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
const route = createRoute({
|
|
136
|
+
method: 'get',
|
|
137
|
+
path: '/users/:id',
|
|
138
|
+
request: {
|
|
139
|
+
params: ParamsSchema,
|
|
140
|
+
},
|
|
141
|
+
responses: {
|
|
72
142
|
400: {
|
|
73
143
|
content: {
|
|
74
144
|
'application/json': {
|
|
75
145
|
schema: ErrorSchema,
|
|
76
146
|
},
|
|
77
147
|
},
|
|
78
|
-
description: '
|
|
148
|
+
description: 'Returns an error',
|
|
79
149
|
},
|
|
80
150
|
},
|
|
81
151
|
})
|
|
82
152
|
```
|
|
83
153
|
|
|
84
|
-
|
|
154
|
+
Finally, add the hook:
|
|
85
155
|
|
|
86
156
|
```ts
|
|
87
|
-
import { OpenAPIHono } from '@hono/zod-openapi'
|
|
88
|
-
|
|
89
|
-
const app = new OpenAPIHono()
|
|
90
|
-
|
|
91
157
|
app.openapi(
|
|
92
158
|
route,
|
|
93
159
|
(c) => {
|
|
@@ -98,31 +164,60 @@ app.openapi(
|
|
|
98
164
|
name: 'Ultra-man',
|
|
99
165
|
})
|
|
100
166
|
},
|
|
167
|
+
// Hook
|
|
101
168
|
(result, c) => {
|
|
102
169
|
if (!result.success) {
|
|
103
|
-
|
|
170
|
+
return c.jsonT(
|
|
104
171
|
{
|
|
105
|
-
|
|
172
|
+
code: 400,
|
|
173
|
+
message: 'Validation Error',
|
|
106
174
|
},
|
|
107
175
|
400
|
|
108
176
|
)
|
|
109
|
-
return res
|
|
110
177
|
}
|
|
111
178
|
}
|
|
112
179
|
)
|
|
180
|
+
```
|
|
113
181
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
182
|
+
### Middleware
|
|
183
|
+
|
|
184
|
+
Zod OpenAPI Hono is an extension of Hono, so you can use Hono's middleware in the same way:
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
import { prettyJSON } from 'hono/pretty-json'
|
|
188
|
+
|
|
189
|
+
//...
|
|
190
|
+
|
|
191
|
+
app.use('/doc/*', prettyJSON())
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### RPC Mode
|
|
195
|
+
|
|
196
|
+
Zod OpenAPI Hono supports Hono's RPC mode. You can define types for the Hono Client as follows:
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
import { hc } from 'hono/client'
|
|
200
|
+
|
|
201
|
+
const appRoutes = app.openapi(route, (c) => {
|
|
202
|
+
const data = c.req.valid('json')
|
|
203
|
+
return c.jsonT({
|
|
204
|
+
id: data.id,
|
|
205
|
+
message: 'Success',
|
|
206
|
+
})
|
|
120
207
|
})
|
|
208
|
+
|
|
209
|
+
const client = hc<typeof appRoutes>('http://localhost:8787/')
|
|
121
210
|
```
|
|
122
211
|
|
|
123
|
-
##
|
|
212
|
+
## References
|
|
213
|
+
|
|
214
|
+
- [Hono](https://hono.dev/)
|
|
215
|
+
- [Zod](https://zod.dev/)
|
|
216
|
+
- [Zod to OpenAPI](https://github.com/asteasolutions/zod-to-openapi)
|
|
217
|
+
|
|
218
|
+
## Authors
|
|
124
219
|
|
|
125
|
-
Yusuke Wada <https://github.com/yusukebe>
|
|
220
|
+
- Yusuke Wada <https://github.com/yusukebe>
|
|
126
221
|
|
|
127
222
|
## License
|
|
128
223
|
|
package/dist/index.cjs
CHANGED
|
@@ -83,7 +83,7 @@ var OpenAPIHono = class extends import_hono.Hono {
|
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
|
-
this.on([route.method], route.path, ...validators, handler);
|
|
86
|
+
this.on([route.method], route.path.replace(/\/{(.+)}/, "/:$1"), ...validators, handler);
|
|
87
87
|
return this;
|
|
88
88
|
};
|
|
89
89
|
this.getOpenAPIDocument = (config) => {
|
package/dist/index.d.cts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as openapi3_ts_oas30 from 'openapi3-ts/oas30';
|
|
2
2
|
import { RouteConfig, ZodRequestBody, ZodContentObject, ResponseConfig } from '@asteasolutions/zod-to-openapi';
|
|
3
3
|
import { OpenAPIObjectConfig } from '@asteasolutions/zod-to-openapi/dist/v3.0/openapi-generator';
|
|
4
|
-
import { Env, Hono, Input, Handler, Context, TypedResponse } from 'hono';
|
|
4
|
+
import { Env, Hono, Input, Handler, Schema, Context, TypedResponse } from 'hono';
|
|
5
5
|
import { AnyZodObject, z, ZodSchema, ZodError, ZodType } from 'zod';
|
|
6
6
|
export { z } from 'zod';
|
|
7
7
|
|
|
@@ -14,18 +14,27 @@ type RequestTypes = {
|
|
|
14
14
|
};
|
|
15
15
|
type IsJson<T> = T extends string ? T extends `application/json${infer _Rest}` ? 'json' : never : never;
|
|
16
16
|
type IsForm<T> = T extends string ? T extends `multipart/form-data${infer _Rest}` | `application/x-www-form-urlencoded${infer _Rest}` ? 'form' : never : never;
|
|
17
|
-
type RequestPart<R extends RouteConfig, Part extends string> = Part extends keyof R['request'] ? R['request'][Part] :
|
|
17
|
+
type RequestPart<R extends RouteConfig, Part extends string> = Part extends keyof R['request'] ? R['request'][Part] : {};
|
|
18
18
|
type InputTypeBase<R extends RouteConfig, Part extends string, Type extends string> = R['request'] extends RequestTypes ? RequestPart<R, Part> extends AnyZodObject ? {
|
|
19
|
+
in: {
|
|
20
|
+
[K in Type]: z.input<RequestPart<R, Part>>;
|
|
21
|
+
};
|
|
19
22
|
out: {
|
|
20
23
|
[K in Type]: z.input<RequestPart<R, Part>>;
|
|
21
24
|
};
|
|
22
25
|
} : {} : {};
|
|
23
26
|
type InputTypeJson<R extends RouteConfig> = R['request'] extends RequestTypes ? R['request']['body'] extends ZodRequestBody ? R['request']['body']['content'] extends ZodContentObject ? IsJson<keyof R['request']['body']['content']> extends never ? {} : R['request']['body']['content'][keyof R['request']['body']['content']]['schema'] extends ZodSchema<any> ? {
|
|
27
|
+
in: {
|
|
28
|
+
json: z.input<R['request']['body']['content'][keyof R['request']['body']['content']]['schema']>;
|
|
29
|
+
};
|
|
24
30
|
out: {
|
|
25
31
|
json: z.input<R['request']['body']['content'][keyof R['request']['body']['content']]['schema']>;
|
|
26
32
|
};
|
|
27
33
|
} : {} : {} : {} : {};
|
|
28
34
|
type InputTypeForm<R extends RouteConfig> = R['request'] extends RequestTypes ? R['request']['body'] extends ZodRequestBody ? R['request']['body']['content'] extends ZodContentObject ? IsForm<keyof R['request']['body']['content']> extends never ? {} : R['request']['body']['content'][keyof R['request']['body']['content']]['schema'] extends ZodSchema<any> ? {
|
|
35
|
+
in: {
|
|
36
|
+
form: z.input<R['request']['body']['content'][keyof R['request']['body']['content']]['schema']>;
|
|
37
|
+
};
|
|
29
38
|
out: {
|
|
30
39
|
form: z.input<R['request']['body']['content'][keyof R['request']['body']['content']]['schema']>;
|
|
31
40
|
};
|
|
@@ -38,10 +47,11 @@ type Hook<T, E extends Env, P extends string, O> = (result: {
|
|
|
38
47
|
success: false;
|
|
39
48
|
error: ZodError;
|
|
40
49
|
}, c: Context<E, P>) => TypedResponse<O> | Promise<TypedResponse<T>> | void;
|
|
50
|
+
type ConvertPathType<T extends string> = T extends `${infer _}/{${infer Param}}${infer _}` ? `/:${Param}` : T;
|
|
41
51
|
declare class OpenAPIHono<E extends Env = Env, S = {}, BasePath extends string = '/'> extends Hono<E, S, BasePath> {
|
|
42
52
|
#private;
|
|
43
53
|
constructor();
|
|
44
|
-
openapi: <R extends RouteConfig, I extends Input = InputTypeBase<R, "params", "param"> & InputTypeBase<R, "query", "query"> & InputTypeForm<R> & InputTypeJson<R>>(route: R, handler: Handler<E,
|
|
54
|
+
openapi: <R extends RouteConfig, I extends Input = InputTypeBase<R, "params", "param"> & InputTypeBase<R, "query", "query"> & InputTypeForm<R> & InputTypeJson<R>, P extends string = ConvertPathType<R["path"]>>(route: R, handler: Handler<E, P, I, OutputType<R>>, hook?: Hook<I, E, P, OutputType<R>> | undefined) => Hono<E, Schema<R["method"], P, I["in"], OutputType<R>>, BasePath>;
|
|
45
55
|
getOpenAPIDocument: (config: OpenAPIObjectConfig) => openapi3_ts_oas30.OpenAPIObject;
|
|
46
56
|
doc: (path: string, config: OpenAPIObjectConfig) => void;
|
|
47
57
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as openapi3_ts_oas30 from 'openapi3-ts/oas30';
|
|
2
2
|
import { RouteConfig, ZodRequestBody, ZodContentObject, ResponseConfig } from '@asteasolutions/zod-to-openapi';
|
|
3
3
|
import { OpenAPIObjectConfig } from '@asteasolutions/zod-to-openapi/dist/v3.0/openapi-generator';
|
|
4
|
-
import { Env, Hono, Input, Handler, Context, TypedResponse } from 'hono';
|
|
4
|
+
import { Env, Hono, Input, Handler, Schema, Context, TypedResponse } from 'hono';
|
|
5
5
|
import { AnyZodObject, z, ZodSchema, ZodError, ZodType } from 'zod';
|
|
6
6
|
export { z } from 'zod';
|
|
7
7
|
|
|
@@ -14,18 +14,27 @@ type RequestTypes = {
|
|
|
14
14
|
};
|
|
15
15
|
type IsJson<T> = T extends string ? T extends `application/json${infer _Rest}` ? 'json' : never : never;
|
|
16
16
|
type IsForm<T> = T extends string ? T extends `multipart/form-data${infer _Rest}` | `application/x-www-form-urlencoded${infer _Rest}` ? 'form' : never : never;
|
|
17
|
-
type RequestPart<R extends RouteConfig, Part extends string> = Part extends keyof R['request'] ? R['request'][Part] :
|
|
17
|
+
type RequestPart<R extends RouteConfig, Part extends string> = Part extends keyof R['request'] ? R['request'][Part] : {};
|
|
18
18
|
type InputTypeBase<R extends RouteConfig, Part extends string, Type extends string> = R['request'] extends RequestTypes ? RequestPart<R, Part> extends AnyZodObject ? {
|
|
19
|
+
in: {
|
|
20
|
+
[K in Type]: z.input<RequestPart<R, Part>>;
|
|
21
|
+
};
|
|
19
22
|
out: {
|
|
20
23
|
[K in Type]: z.input<RequestPart<R, Part>>;
|
|
21
24
|
};
|
|
22
25
|
} : {} : {};
|
|
23
26
|
type InputTypeJson<R extends RouteConfig> = R['request'] extends RequestTypes ? R['request']['body'] extends ZodRequestBody ? R['request']['body']['content'] extends ZodContentObject ? IsJson<keyof R['request']['body']['content']> extends never ? {} : R['request']['body']['content'][keyof R['request']['body']['content']]['schema'] extends ZodSchema<any> ? {
|
|
27
|
+
in: {
|
|
28
|
+
json: z.input<R['request']['body']['content'][keyof R['request']['body']['content']]['schema']>;
|
|
29
|
+
};
|
|
24
30
|
out: {
|
|
25
31
|
json: z.input<R['request']['body']['content'][keyof R['request']['body']['content']]['schema']>;
|
|
26
32
|
};
|
|
27
33
|
} : {} : {} : {} : {};
|
|
28
34
|
type InputTypeForm<R extends RouteConfig> = R['request'] extends RequestTypes ? R['request']['body'] extends ZodRequestBody ? R['request']['body']['content'] extends ZodContentObject ? IsForm<keyof R['request']['body']['content']> extends never ? {} : R['request']['body']['content'][keyof R['request']['body']['content']]['schema'] extends ZodSchema<any> ? {
|
|
35
|
+
in: {
|
|
36
|
+
form: z.input<R['request']['body']['content'][keyof R['request']['body']['content']]['schema']>;
|
|
37
|
+
};
|
|
29
38
|
out: {
|
|
30
39
|
form: z.input<R['request']['body']['content'][keyof R['request']['body']['content']]['schema']>;
|
|
31
40
|
};
|
|
@@ -38,10 +47,11 @@ type Hook<T, E extends Env, P extends string, O> = (result: {
|
|
|
38
47
|
success: false;
|
|
39
48
|
error: ZodError;
|
|
40
49
|
}, c: Context<E, P>) => TypedResponse<O> | Promise<TypedResponse<T>> | void;
|
|
50
|
+
type ConvertPathType<T extends string> = T extends `${infer _}/{${infer Param}}${infer _}` ? `/:${Param}` : T;
|
|
41
51
|
declare class OpenAPIHono<E extends Env = Env, S = {}, BasePath extends string = '/'> extends Hono<E, S, BasePath> {
|
|
42
52
|
#private;
|
|
43
53
|
constructor();
|
|
44
|
-
openapi: <R extends RouteConfig, I extends Input = InputTypeBase<R, "params", "param"> & InputTypeBase<R, "query", "query"> & InputTypeForm<R> & InputTypeJson<R>>(route: R, handler: Handler<E,
|
|
54
|
+
openapi: <R extends RouteConfig, I extends Input = InputTypeBase<R, "params", "param"> & InputTypeBase<R, "query", "query"> & InputTypeForm<R> & InputTypeJson<R>, P extends string = ConvertPathType<R["path"]>>(route: R, handler: Handler<E, P, I, OutputType<R>>, hook?: Hook<I, E, P, OutputType<R>> | undefined) => Hono<E, Schema<R["method"], P, I["in"], OutputType<R>>, BasePath>;
|
|
45
55
|
getOpenAPIDocument: (config: OpenAPIObjectConfig) => openapi3_ts_oas30.OpenAPIObject;
|
|
46
56
|
doc: (path: string, config: OpenAPIObjectConfig) => void;
|
|
47
57
|
}
|
package/dist/index.js
CHANGED
|
@@ -58,7 +58,7 @@ var OpenAPIHono = class extends Hono {
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
|
-
this.on([route.method], route.path, ...validators, handler);
|
|
61
|
+
this.on([route.method], route.path.replace(/\/{(.+)}/, "/:$1"), ...validators, handler);
|
|
62
62
|
return this;
|
|
63
63
|
};
|
|
64
64
|
this.getOpenAPIDocument = (config) => {
|