@sdk-it/hono 0.46.0 → 0.46.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.
Files changed (2) hide show
  1. package/README.md +95 -194
  2. package/package.json +2 -2
package/README.md CHANGED
@@ -1,134 +1,99 @@
1
1
  # @sdk-it/hono
2
2
 
3
- Hono framework integration for SDK-IT that provides type-safe request validation and semantic response handling.
3
+ Hono runtime middleware and response analysis for SDK-IT.
4
4
 
5
- To learn more about SDK code generation, see the [TypeScript Doc](../typescript/readme.md)
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/{hono,generic}
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 Primitives
15
+ ## Runtime primitives
14
16
 
15
- You can use these functions without the SDK-IT code generation tools -- they're completely separate and functional on their own.
17
+ The runtime exports work without generating an SDK.
16
18
 
17
- ### Validator Middleware
19
+ ### Validate requests
18
20
 
19
- The validator middleware validates requests against [Zod](https://github.com/colinhacks/zod) schemas and provides typed inputs to your handlers.
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
- // No content type specified - validation runs regardless of content type
37
- validate((payload) => ({
38
- // Query parameter validation
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
- // Body property validation
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
- // TypeScript knows the shape of all inputs
84
- const { page, categories, title, author, metadata, userId, apiKey } =
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
- **Enforcing Content Type:**
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
- import { z } from 'zod';
97
-
98
- import { validate } from '@sdk-it/hono/runtime';
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
- **Handling File Uploads (`multipart/form-data`):**
82
+ Supported enforced content types are:
118
83
 
119
- Use `z.instanceof(File)` to validate file uploads when enforcing `multipart/form-data`.
84
+ - `application/json`
85
+ - `application/x-www-form-urlencoded`
86
+ - `multipart/form-data`
87
+ - `text/plain`
120
88
 
121
- ```typescript
122
- import { z } from 'zod';
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
- import { validate } from '@sdk-it/hono/runtime';
92
+ ### Validate file uploads
125
93
 
126
- // import { writeFile } from 'node:fs/promises'; // Example for saving file
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, // 'avatar' is the field name in the form data
142
- against: z.instanceof(File), // <-- Validate that 'avatar' is a File object
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(), // Example: optional caption field
110
+ against: z.string().optional(),
148
111
  },
149
112
  })),
150
- async (c) => {
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
- message: `Avatar for user ${userId} uploaded successfully.`,
116
+ userId,
117
+ filename: avatar.name,
118
+ size: avatar.size,
119
+ caption,
165
120
  });
166
121
  },
167
122
  );
168
123
  ```
169
124
 
170
- ### Content Type Consumption
125
+ ### Enforce a content type without validation
171
126
 
172
- If you only need to enforce a content type without performing validation, use the `consume` middleware:
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
- '/upload',
179
- consume('multipart/form-data'), // <-- Enforces 'multipart/form-data'
180
- async (c) => {
181
- // Process raw multipart form data from the request body
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
- ### Response Helper
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
- > [!NOTE]
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
- ## OpenAPI Generation
224
-
225
- SDK-IT relies on the `validator` middleware and JSDoc to correctly infer each route specification.
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
- Consider the following example:
157
+ ## Generate OpenAPI and a client
228
158
 
229
- - Create hono routes with the `@openapi` tag and validate middleware.
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
- async (c) => {
251
- const { author } = c.var.input; // <-- Access validated input
252
- const books = [{ name: `Books by ${author}` }];
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
- > [!TIP]
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 { join } from 'node:path';
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('apps/backend/tsconfig.app.json', {
282
- responseAnalyzer,
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: join(process.cwd(), './client'),
220
+ output: resolve('client'),
221
+ name: 'Client',
302
222
  });
303
223
  ```
304
224
 
305
- - Run the script
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 --watch-path ./apps/backend/src --watch ./openapi.ts
228
+ node openapi.ts
323
229
  ```
324
230
 
325
- </details>
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, UnauthorizedError } from './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('Books retrieved:', books);
244
+ console.log(books);
344
245
  } catch (error) {
345
- if (error instanceof UnauthorizedError) {
346
- console.error('Unauthorized access - perhaps you need to log in?');
246
+ if (error instanceof Unauthorized) {
247
+ console.error('Authentication is required', error.data);
347
248
  } else {
348
- console.error('Error fetching books:', error);
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.0",
3
+ "version": "0.46.1",
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.0",
30
+ "@sdk-it/core": "0.46.1",
31
31
  "hono": "^4.7.4",
32
32
  "zod": "^4.3.0",
33
33
  "fast-content-type-parse": "^3.0.0"