@sdk-it/hono 0.46.0 → 0.46.2
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 +95 -194
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,134 +1,99 @@
|
|
|
1
1
|
# @sdk-it/hono
|
|
2
2
|
|
|
3
|
-
Hono
|
|
3
|
+
Hono runtime middleware and response analysis for SDK-IT.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
See [`@sdk-it/typescript`](../typescript/README.md) for OpenAPI-to-TypeScript
|
|
6
|
+
client generation.
|
|
6
7
|
|
|
7
8
|
## Installation
|
|
8
9
|
|
|
9
10
|
```bash
|
|
10
|
-
npm install @sdk-it/
|
|
11
|
+
npm install @sdk-it/hono hono zod
|
|
12
|
+
npm install --save-dev @sdk-it/generic @sdk-it/typescript typescript@^6.0.3
|
|
11
13
|
```
|
|
12
14
|
|
|
13
|
-
## Runtime
|
|
15
|
+
## Runtime primitives
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
The runtime exports work without generating an SDK.
|
|
16
18
|
|
|
17
|
-
###
|
|
19
|
+
### Validate requests
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
> [!IMPORTANT]
|
|
22
|
-
> For openapi generation to work correctly, you must use the `validate` middleware for each route.
|
|
23
|
-
|
|
24
|
-
> [!TIP]
|
|
25
|
-
> You can copy paste the middleware to your project if you want customize it further.
|
|
26
|
-
|
|
27
|
-
**Basic Usage:**
|
|
21
|
+
Every route included in OpenAPI generation must use `validate`. The middleware
|
|
22
|
+
validates the selected values and exposes the parsed result as `c.var.input`.
|
|
28
23
|
|
|
29
24
|
```typescript
|
|
25
|
+
import { Hono } from 'hono';
|
|
30
26
|
import { z } from 'zod';
|
|
31
27
|
|
|
32
28
|
import { validate } from '@sdk-it/hono/runtime';
|
|
33
29
|
|
|
30
|
+
const app = new Hono();
|
|
31
|
+
|
|
34
32
|
app.post(
|
|
35
|
-
'/books',
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
'/users/:userId/books',
|
|
34
|
+
validate('application/json', (payload) => ({
|
|
35
|
+
userId: {
|
|
36
|
+
select: payload.params.userId,
|
|
37
|
+
against: z.uuid(),
|
|
38
|
+
},
|
|
39
39
|
page: {
|
|
40
40
|
select: payload.query.page,
|
|
41
|
-
against: z.number().min(1).default(1),
|
|
41
|
+
against: z.coerce.number().int().min(1).default(1),
|
|
42
42
|
},
|
|
43
|
-
|
|
44
|
-
// Multiple query parameters (array)
|
|
45
43
|
categories: {
|
|
46
44
|
select: payload.queries.category,
|
|
47
45
|
against: z.array(z.string()),
|
|
48
46
|
},
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
apiKey: {
|
|
48
|
+
select: payload.headers['x-api-key'],
|
|
49
|
+
against: z.string().min(32),
|
|
50
|
+
},
|
|
51
51
|
title: {
|
|
52
52
|
select: payload.body.title,
|
|
53
53
|
against: z.string().min(1),
|
|
54
54
|
},
|
|
55
|
-
|
|
56
|
-
author: {
|
|
57
|
-
select: payload.body.author,
|
|
58
|
-
against: z.string().min(1),
|
|
59
|
-
},
|
|
60
|
-
|
|
61
|
-
// For nested objects in body
|
|
62
55
|
metadata: {
|
|
63
56
|
select: payload.body.metadata,
|
|
64
57
|
against: z.object({
|
|
65
58
|
isbn: z.string(),
|
|
66
|
-
publishedYear: z.number(),
|
|
59
|
+
publishedYear: z.number().int(),
|
|
67
60
|
}),
|
|
68
61
|
},
|
|
69
|
-
|
|
70
|
-
// URL parameter validation
|
|
71
|
-
userId: {
|
|
72
|
-
select: payload.params.userId,
|
|
73
|
-
against: z.uuid(),
|
|
74
|
-
},
|
|
75
|
-
|
|
76
|
-
// Header validation
|
|
77
|
-
apiKey: {
|
|
78
|
-
select: payload.headers['x-api-key'],
|
|
79
|
-
against: z.string().min(32),
|
|
80
|
-
},
|
|
81
62
|
})),
|
|
82
63
|
(c) => {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
c.var.input;
|
|
86
|
-
return c.json({ success: true });
|
|
64
|
+
const { userId, page, categories, apiKey, title, metadata } = c.var.input;
|
|
65
|
+
return c.json({ userId, page, categories, apiKey, title, metadata }, 201);
|
|
87
66
|
},
|
|
88
67
|
);
|
|
89
68
|
```
|
|
90
69
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
Pass a content type as the first argument to enforce it before validation:
|
|
70
|
+
The optional first argument enforces the request content type before
|
|
71
|
+
validation:
|
|
94
72
|
|
|
95
73
|
```typescript
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
app.post(
|
|
101
|
-
'/users',
|
|
102
|
-
validate('application/json', (payload) => ({
|
|
103
|
-
// <-- Enforces 'application/json'
|
|
104
|
-
name: {
|
|
105
|
-
select: payload.body.name,
|
|
106
|
-
against: z.string(),
|
|
107
|
-
},
|
|
108
|
-
})),
|
|
109
|
-
(c) => {
|
|
110
|
-
// Handle request with guaranteed JSON content
|
|
111
|
-
const { name } = c.var.input;
|
|
112
|
-
return c.json({ success: true });
|
|
74
|
+
validate('application/json', (payload) => ({
|
|
75
|
+
name: {
|
|
76
|
+
select: payload.body.name,
|
|
77
|
+
against: z.string(),
|
|
113
78
|
},
|
|
114
|
-
);
|
|
79
|
+
}));
|
|
115
80
|
```
|
|
116
81
|
|
|
117
|
-
|
|
82
|
+
Supported enforced content types are:
|
|
118
83
|
|
|
119
|
-
|
|
84
|
+
- `application/json`
|
|
85
|
+
- `application/x-www-form-urlencoded`
|
|
86
|
+
- `multipart/form-data`
|
|
87
|
+
- `text/plain`
|
|
120
88
|
|
|
121
|
-
|
|
122
|
-
|
|
89
|
+
Query and path values arrive as strings. Use Zod coercion when the parsed value
|
|
90
|
+
should be a number, boolean, or another non-string type.
|
|
123
91
|
|
|
124
|
-
|
|
92
|
+
### Validate file uploads
|
|
125
93
|
|
|
126
|
-
|
|
94
|
+
Use `z.instanceof(File)` with `multipart/form-data`:
|
|
127
95
|
|
|
128
|
-
|
|
129
|
-
* @openapi uploadProfilePicture
|
|
130
|
-
* @tags users
|
|
131
|
-
*/
|
|
96
|
+
```typescript
|
|
132
97
|
app.post(
|
|
133
98
|
'/users/:userId/avatar',
|
|
134
99
|
validate('multipart/form-data', (payload) => ({
|
|
@@ -136,104 +101,71 @@ app.post(
|
|
|
136
101
|
select: payload.params.userId,
|
|
137
102
|
against: z.uuid(),
|
|
138
103
|
},
|
|
139
|
-
// File validation
|
|
140
104
|
avatar: {
|
|
141
|
-
select: payload.body.avatar,
|
|
142
|
-
against: z.instanceof(File),
|
|
105
|
+
select: payload.body.avatar,
|
|
106
|
+
against: z.instanceof(File),
|
|
143
107
|
},
|
|
144
|
-
// Other form fields can also be validated
|
|
145
108
|
caption: {
|
|
146
109
|
select: payload.body.caption,
|
|
147
|
-
against: z.string().optional(),
|
|
110
|
+
against: z.string().optional(),
|
|
148
111
|
},
|
|
149
112
|
})),
|
|
150
|
-
|
|
113
|
+
(c) => {
|
|
151
114
|
const { userId, avatar, caption } = c.var.input;
|
|
152
|
-
|
|
153
|
-
// Example: Process the uploaded file
|
|
154
|
-
// const fileBuffer = Buffer.from(await avatar.arrayBuffer());
|
|
155
|
-
// await writeFile(`./uploads/${userId}_${avatar.name}`, fileBuffer);
|
|
156
|
-
|
|
157
|
-
console.log(
|
|
158
|
-
`Received avatar for user ${userId}: ${avatar.name}, size: ${avatar.size}`,
|
|
159
|
-
);
|
|
160
|
-
if (caption) {
|
|
161
|
-
console.log(`Caption: ${caption}`);
|
|
162
|
-
}
|
|
163
115
|
return c.json({
|
|
164
|
-
|
|
116
|
+
userId,
|
|
117
|
+
filename: avatar.name,
|
|
118
|
+
size: avatar.size,
|
|
119
|
+
caption,
|
|
165
120
|
});
|
|
166
121
|
},
|
|
167
122
|
);
|
|
168
123
|
```
|
|
169
124
|
|
|
170
|
-
###
|
|
125
|
+
### Enforce a content type without validation
|
|
171
126
|
|
|
172
|
-
|
|
127
|
+
Use `consume` when a route only needs content-type enforcement:
|
|
173
128
|
|
|
174
129
|
```typescript
|
|
175
130
|
import { consume } from '@sdk-it/hono/runtime';
|
|
176
131
|
|
|
177
|
-
app.post(
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const body = await c.req.parseBody();
|
|
183
|
-
const file = body['file']; // Access file data
|
|
184
|
-
// ... process file ...
|
|
185
|
-
return c.json({ success: true });
|
|
186
|
-
},
|
|
187
|
-
);
|
|
132
|
+
app.post('/upload', consume('multipart/form-data'), async (c) => {
|
|
133
|
+
const body = await c.req.parseBody();
|
|
134
|
+
const file = body.file;
|
|
135
|
+
return c.json({ uploaded: file instanceof File });
|
|
136
|
+
});
|
|
188
137
|
```
|
|
189
138
|
|
|
190
|
-
###
|
|
191
|
-
|
|
192
|
-
The output function sends HTTP responses with typed status codes and content types.
|
|
193
|
-
|
|
194
|
-
The `output` utility builds on hono's `context.body`.
|
|
139
|
+
### Send semantic responses
|
|
195
140
|
|
|
196
|
-
|
|
197
|
-
> You don't necessarily need to use this function for OpenAPI generation, but it provides a clean and consistent way to send responses.
|
|
141
|
+
`createOutput` wraps Hono's response methods:
|
|
198
142
|
|
|
199
143
|
```typescript
|
|
200
144
|
import { createOutput } from '@sdk-it/hono/runtime';
|
|
201
145
|
|
|
202
146
|
app.post('/users', (c) => {
|
|
203
147
|
const output = createOutput(() => c);
|
|
204
|
-
|
|
205
|
-
// Success responses
|
|
206
|
-
return output.ok({ data: 'success' });
|
|
207
|
-
return output.accepted({ status: 'processing' });
|
|
208
|
-
|
|
209
|
-
// Error responses
|
|
210
|
-
return output.badRequest({ error: 'Invalid input' });
|
|
211
|
-
return output.unauthorized({ error: 'Not authenticated' });
|
|
212
|
-
return output.forbidden({ error: 'Not authorized' });
|
|
213
|
-
return output.notImplemented({ error: 'Coming soon' });
|
|
214
|
-
|
|
215
|
-
// Redirects
|
|
216
|
-
return output.redirect('/new-location');
|
|
217
|
-
|
|
218
|
-
// Custom headers
|
|
219
|
-
return output.ok({ data: 'success' }, { 'Cache-Control': 'max-age=3600' });
|
|
148
|
+
return output.created('/users/123', { id: '123' });
|
|
220
149
|
});
|
|
221
150
|
```
|
|
222
151
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
152
|
+
The helper provides methods for common success and error statuses, redirects,
|
|
153
|
+
attachments, and custom headers. It is a runtime utility; use Hono response
|
|
154
|
+
methods such as `c.json` on analyzed routes so `responseAnalyzer` can infer
|
|
155
|
+
their response bodies and status codes.
|
|
226
156
|
|
|
227
|
-
|
|
157
|
+
## Generate OpenAPI and a client
|
|
228
158
|
|
|
229
|
-
|
|
159
|
+
Create an analyzed route:
|
|
230
160
|
|
|
231
161
|
```typescript
|
|
162
|
+
// src/app.ts
|
|
163
|
+
import { Hono } from 'hono';
|
|
232
164
|
import { z } from 'zod';
|
|
233
165
|
|
|
234
166
|
import { validate } from '@sdk-it/hono/runtime';
|
|
235
167
|
|
|
236
|
-
const app = new Hono();
|
|
168
|
+
export const app = new Hono();
|
|
237
169
|
|
|
238
170
|
/**
|
|
239
171
|
* @openapi listBooks
|
|
@@ -247,90 +179,59 @@ app.get(
|
|
|
247
179
|
against: z.string(),
|
|
248
180
|
},
|
|
249
181
|
})),
|
|
250
|
-
|
|
251
|
-
const { author } = c.var.input;
|
|
252
|
-
|
|
253
|
-
return c.json(books);
|
|
182
|
+
(c) => {
|
|
183
|
+
const { author } = c.var.input;
|
|
184
|
+
return c.json([{ title: 'Example', author }]);
|
|
254
185
|
},
|
|
255
186
|
);
|
|
256
187
|
```
|
|
257
188
|
|
|
258
|
-
|
|
259
|
-
> Instead of using `createOutput` fn, you can use [context-storage](https://hono.dev/docs/middleware/builtin/context-storage) middleware and then import the global `output` object from `@sdk-it/hono/runtime`.
|
|
260
|
-
|
|
261
|
-
- Use the generate fn to create an OpenAPI spec from your routes.
|
|
262
|
-
|
|
263
|
-
<b><small>filename: openapi.ts</small></b>
|
|
189
|
+
Analyze the backend and generate the client:
|
|
264
190
|
|
|
265
191
|
```typescript
|
|
192
|
+
// openapi.ts
|
|
266
193
|
import { writeFile } from 'node:fs/promises';
|
|
267
|
-
import {
|
|
194
|
+
import { resolve } from 'node:path';
|
|
268
195
|
|
|
269
196
|
import { analyze } from '@sdk-it/generic';
|
|
270
|
-
// Use responseAnalyzer from `@sdk-it/hono`
|
|
271
|
-
// only if you use hono context object to send response
|
|
272
|
-
// e.g. c.json({ data: 'success' });
|
|
273
197
|
import { responseAnalyzer } from '@sdk-it/hono';
|
|
274
|
-
// Use responseAnalyzer from `@sdk-it/generic`
|
|
275
|
-
// only if you use the output function to send response
|
|
276
|
-
// e.g. output.ok({ data: 'success' });
|
|
277
|
-
// import { responseAnalyzer } from '@sdk-it/generic';
|
|
278
|
-
|
|
279
198
|
import { generate } from '@sdk-it/typescript';
|
|
280
199
|
|
|
281
|
-
const { paths, components } = await analyze(
|
|
282
|
-
|
|
283
|
-
|
|
200
|
+
const { paths, components, tags } = await analyze(
|
|
201
|
+
'./apps/backend/tsconfig.app.json',
|
|
202
|
+
{
|
|
203
|
+
responseAnalyzer,
|
|
204
|
+
},
|
|
205
|
+
);
|
|
284
206
|
|
|
285
|
-
// Now you can use the generated specification to create an SDK or save it to a file
|
|
286
207
|
const spec = {
|
|
208
|
+
openapi: '3.1.0' as const,
|
|
287
209
|
info: {
|
|
288
210
|
title: 'My API',
|
|
289
211
|
version: '1.0.0',
|
|
290
212
|
},
|
|
291
213
|
paths,
|
|
292
214
|
components,
|
|
215
|
+
tags: tags.map((name) => ({ name })),
|
|
293
216
|
};
|
|
294
217
|
|
|
295
|
-
// Save the spec to a file
|
|
296
218
|
await writeFile('openapi.json', JSON.stringify(spec, null, 2));
|
|
297
|
-
// OR
|
|
298
|
-
|
|
299
|
-
// Continue to generate an SDK
|
|
300
219
|
await generate(spec, {
|
|
301
|
-
output:
|
|
220
|
+
output: resolve('client'),
|
|
221
|
+
name: 'Client',
|
|
302
222
|
});
|
|
303
223
|
```
|
|
304
224
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
```bash
|
|
308
|
-
# using recent versions of node
|
|
309
|
-
node ./openapi.ts
|
|
310
|
-
|
|
311
|
-
# using node < 22
|
|
312
|
-
npx tsx ./openapi.ts
|
|
313
|
-
|
|
314
|
-
# using bun
|
|
315
|
-
bun ./openapi.ts
|
|
316
|
-
```
|
|
317
|
-
|
|
318
|
-
<details>
|
|
319
|
-
<summary> Run in watch mode </summary>
|
|
225
|
+
Run the script with Node.js 24 or newer:
|
|
320
226
|
|
|
321
227
|
```bash
|
|
322
|
-
node
|
|
228
|
+
node openapi.ts
|
|
323
229
|
```
|
|
324
230
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
> [!TIP]
|
|
328
|
-
> See [the typescript package](../typescript/README.md) for more info.
|
|
329
|
-
|
|
330
|
-
- Use the client
|
|
231
|
+
The generated client returns response data and throws typed errors:
|
|
331
232
|
|
|
332
233
|
```typescript
|
|
333
|
-
import { Client,
|
|
234
|
+
import { Client, Unauthorized } from './client/index.ts';
|
|
334
235
|
|
|
335
236
|
const client = new Client({
|
|
336
237
|
baseUrl: 'http://localhost:3000',
|
|
@@ -340,12 +241,12 @@ try {
|
|
|
340
241
|
const books = await client.request('GET /books', {
|
|
341
242
|
author: 'John Doe',
|
|
342
243
|
});
|
|
343
|
-
console.log(
|
|
244
|
+
console.log(books);
|
|
344
245
|
} catch (error) {
|
|
345
|
-
if (error instanceof
|
|
346
|
-
console.error('
|
|
246
|
+
if (error instanceof Unauthorized) {
|
|
247
|
+
console.error('Authentication is required', error.data);
|
|
347
248
|
} else {
|
|
348
|
-
|
|
249
|
+
throw error;
|
|
349
250
|
}
|
|
350
251
|
}
|
|
351
252
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdk-it/hono",
|
|
3
|
-
"version": "0.46.
|
|
3
|
+
"version": "0.46.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"!**/*.test.*"
|
|
28
28
|
],
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@sdk-it/core": "0.46.
|
|
30
|
+
"@sdk-it/core": "0.46.2",
|
|
31
31
|
"hono": "^4.7.4",
|
|
32
32
|
"zod": "^4.3.0",
|
|
33
33
|
"fast-content-type-parse": "^3.0.0"
|