@nxgt/openapi-hono 0.1.0

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 ADDED
@@ -0,0 +1,692 @@
1
+ # @nxgt/openapi-hono
2
+
3
+ Typed Hono routes for an OpenAPI spec. This is the runtime that the
4
+ `hono.ts` file generated by
5
+ [`@nxgt/openapi-codegen`](https://github.com/softistx/nxgt-http/blob/develop/packages/openapi-codegen/README.md) binds to its spec:
6
+ - each route validates its request with the generated validators before the
7
+ handler runs;
8
+ - a request the spec refuses gets a 400 that lists every issue;
9
+ - a reply the spec does not declare does not compile.
10
+
11
+ > It replaces `@nxgt/openapi-codegen/hono`, the subpath that held this
12
+ > runtime in codegen 0.1.0. Code generated by codegen 0.2.0 and later
13
+ > imports `@nxgt/openapi-hono` instead.
14
+
15
+ ## Install
16
+
17
+ ```sh
18
+ bun add @nxgt/openapi-hono hono zod
19
+ bun add -d @nxgt/openapi-codegen typescript
20
+ ```
21
+
22
+ - `hono` is a required peer. This package imports it for types only; your
23
+ app imports it to build the app.
24
+ - `typescript` 6 is a required peer, as for every `@nxgt` package.
25
+ - `zod` is not a peer, but the app needs it: the generated `operations.ts`
26
+ imports it.
27
+ - The generator is only a dev dependency.
28
+
29
+ ## Setup
30
+
31
+ Generate with the `hono` option:
32
+
33
+ ```ts
34
+ // openapi-codegen.config.ts
35
+ import { defineConfig } from '@nxgt/openapi-codegen';
36
+
37
+ export default defineConfig({
38
+ input: 'openapi/openapi.yaml',
39
+ output: 'src/generated',
40
+ hono: true,
41
+ });
42
+ ```
43
+
44
+ `bunx nxgt-openapi generate` then writes `hono.ts` beside the other
45
+ generated files. Import `createRoutes` and `createApi` from that file, not
46
+ from this package: there they are bound to your spec.
47
+
48
+ ## Usage
49
+
50
+ ### Registering routes
51
+
52
+ ```ts
53
+ import { Hono } from 'hono';
54
+ import { createRoutes } from './generated/hono.js';
55
+
56
+ const app = new Hono();
57
+ const routes = createRoutes(app);
58
+
59
+ routes.put('/employees/{id}', auth, async (c) => {
60
+ const { id } = c.req.valid('param');
61
+ const employee = await employees.update(id, c.req.valid('json'));
62
+ if (!employee) return c.json({ message: 'errors.not-found' }, 404);
63
+ return c.json(employee, 200);
64
+ });
65
+
66
+ routes.operation('deleteEmployee', auth, async (c) => {
67
+ await employees.remove(c.req.valid('param').id);
68
+ return c.body(null, 204);
69
+ });
70
+ ```
71
+
72
+ - **Paths are written as the spec writes them:** `{id}`, not `:id`. Each
73
+ method offers only the paths that have an operation for it.
74
+ - **`c.req.valid()`** holds `param`, `query`, `header`, and `json` or `form`
75
+ for a body, already validated. A text body is validated too; read it with
76
+ `c.req.text()`.
77
+ - **A route runs as `[...middlewares, validator, handler]`**, so a 401 from
78
+ `auth` comes before a 400.
79
+ - **`routes.validate`** marks where validation runs, for a middleware that
80
+ needs validated input: `routes.put(path, auth, routes.validate, owns, handler)`.
81
+
82
+ ### Validation errors
83
+
84
+ By default, a request the spec refuses gets a 400 response. Its body is
85
+ `{ status, message: 'errors.validation-failed', timestamp, issues }`, and
86
+ each issue is `{ target, path, code, message }`. To answer differently:
87
+
88
+ ```ts
89
+ const routes = createRoutes(app, {
90
+ onValidationError: (failure, c) => {
91
+ throw new CustomException(400, 'errors.validation-failed', {
92
+ issues: failure.issues,
93
+ });
94
+ },
95
+ });
96
+ ```
97
+
98
+ The hook can:
99
+ - return a `Response`, which is sent;
100
+ - throw, which hands the failure to `app.onError`;
101
+ - return nothing, which sends the default.
102
+
103
+ `routes.with({ onValidationError })` sets the hook for the routes registered
104
+ through it only.
105
+
106
+ ### Checking replies
107
+
108
+ ```ts
109
+ const routes = createRoutes(app, {
110
+ validateResponses: process.env.NODE_ENV !== 'production',
111
+ });
112
+ ```
113
+
114
+ Every reply is checked against the spec: its status, its `Content-Type`,
115
+ and, for JSON and text, its body. A reply that fails goes through
116
+ `onValidationError`, as a 500 by default, its issues logged with
117
+ `console.error` rather than sent. The check reads every reply body twice,
118
+ so keep it for development and tests.
119
+
120
+ ### Streams
121
+
122
+ An operation whose reply the spec describes an item at a time, with OpenAPI
123
+ 3.2's `itemSchema`, streams it from its handler with the `hono.ts` helpers:
124
+
125
+ ```ts
126
+ import { createRoutes, streamEvents, streamLines } from './generated/hono.js';
127
+
128
+ createRoutes(app)
129
+ .get('/feed', (c) =>
130
+ streamEvents(c, 'watchFeed', async (stream) => {
131
+ await stream.write({ event: 'update', id: '7', data: item }); // Item, as JSON
132
+ await stream.write({ event: 'ping', data: 'still here' }); // text
133
+ while (!stream.aborted) await stream.sleep(15_000);
134
+ }),
135
+ )
136
+ .post('/export', (c) =>
137
+ streamLines(c, 'exportItems', async (stream) => {
138
+ for (const item of items) await stream.write(item);
139
+ }),
140
+ );
141
+ ```
142
+
143
+ - Each event is typed by its name, and its data is sent as JSON when the
144
+ spec declares it JSON, as text otherwise. JSON lines go out as the media
145
+ type the spec declares.
146
+ - With `validateResponses`, each item is checked before it is sent. The
147
+ status is out already, so a failing item goes to `onValidationError` for
148
+ its side effects, is not sent, and ends the stream.
149
+ - `stream.aborted` and `stream.onAbort()` tell when the client went away.
150
+
151
+ ### Modules
152
+
153
+ ```ts
154
+ import { createApi } from './generated/hono.js';
155
+
156
+ export const api = createApi();
157
+
158
+ // employees.routes.ts
159
+ const employees = new Hono();
160
+ api
161
+ .routes(employees, { prefix: '/employees', tag: 'employees' })
162
+ .get('/employees', listEmployees)
163
+ .get('/employees/{id}', getEmployee);
164
+
165
+ // app.ts
166
+ app.route('/employees', employees);
167
+ api.assertComplete(); // throws, listing every operation without a route
168
+ ```
169
+
170
+ - **`prefix`** is where the sub-app is mounted. Only paths under it are
171
+ offered.
172
+ - **`tag`** offers only the operations that carry that tag.
173
+ - **`api.missing(tag?)`** lists the operations that have no route.
174
+ - **`api.assertComplete(tag?)`** throws when any operation has no route.
175
+
176
+ `createRoutes(app, options)` is `createApi(options).routes(app, options)`:
177
+ a registry for that one app.
178
+
179
+ Registering a route throws at startup in these cases:
180
+ - the spec has no operation at that method and path, or no such
181
+ `operationId`;
182
+ - the operation already has a route;
183
+ - an earlier route would always answer first;
184
+ - the operation is outside the `prefix` or the `tag`;
185
+ - the last argument is not the handler, or `routes.validate` appears twice;
186
+ - Hono cannot route it: a `HEAD` operation (Hono answers it with the `GET`
187
+ route), or a path parameter that does not fill its segment
188
+ (`/files/{name}.json`). Generating warns about both, and `missing()`
189
+ leaves them out.
190
+
191
+ ## API
192
+
193
+ The app imports from its generated `hono.ts`, which binds this package to
194
+ the spec. The package's own exports are what `hono.ts` is built from, and
195
+ the types of a failure for your own `onValidationError`.
196
+
197
+ ### Generated `hono.ts`
198
+
199
+ Generated with `hono: true`. `S` below is its `HonoSpec`.
200
+
201
+ #### `createApi()`
202
+
203
+ ```ts
204
+ const createApi: (options?: ApiOptions) => Api<HonoSpec>;
205
+ ```
206
+
207
+ One registry for the whole spec: `api.routes(app)` in each module, then
208
+ `api.assertComplete()`. `options` are the defaults of every `routes()` it
209
+ makes ([`ApiOptions`](#apioptions)). See [Modules](#modules).
210
+
211
+ #### `createRoutes()`
212
+
213
+ ```ts
214
+ const createRoutes: <Prefix extends string = '', Tag extends keyof OperationsByTag & string = never>(
215
+ app: Hono<any, any, any>,
216
+ options?: RoutesOptions<Prefix, Tag>,
217
+ ) => Routes<HonoSpec, ScopeOf<HonoSpec, Tag>, Prefix>;
218
+ ```
219
+
220
+ Routes on one app, with a registry of their own: `createApi(options).routes(app, options)`.
221
+ Takes [`RoutesOptions`](#routesoptions) and returns [`Routes`](#routes).
222
+ See [Registering routes](#registering-routes).
223
+
224
+ #### `streamEvents()`
225
+
226
+ ```ts
227
+ const streamEvents: <Id extends /* each operation that replies with server-sent events */>(
228
+ c: Context<any, any, any>,
229
+ id: Id,
230
+ write: (stream: EventWriter<Operations[Id]['stream']['item']>) => Promise<void>,
231
+ ) => Replies[Id];
232
+ ```
233
+
234
+ Replies with the operation's server-sent events, each typed by the spec.
235
+ Generated only when the spec has such an operation. Behaves as the
236
+ package's [`streamEvents()`](#streamevents-1). See [Streams](#streams).
237
+
238
+ #### `streamLines()`
239
+
240
+ ```ts
241
+ const streamLines: <Id extends /* each operation that replies with JSON Lines */>(
242
+ c: Context<any, any, any>,
243
+ id: Id,
244
+ write: (stream: LineWriter<Operations[Id]['stream']['item']>) => Promise<void>,
245
+ ) => Replies[Id];
246
+ ```
247
+
248
+ Replies with the operation's JSON lines, each typed by the spec. Generated
249
+ only when the spec has such an operation. Behaves as the package's
250
+ [`streamLines()`](#streamlines-1).
251
+
252
+ #### `Replies`
253
+
254
+ ```ts
255
+ interface Replies {
256
+ updateEmployee:
257
+ | TypedResponse<Employee, 200, 'json'>
258
+ | TypedResponse<Problem, 404, 'json'>;
259
+ deleteEmployee: TypedResponse<null, 204, 'body'>;
260
+ }
261
+ ```
262
+
263
+ What each operation may reply, keyed by `operationId`: a handler returning
264
+ anything else does not compile. A JSON body is typed as JSON carries it, a
265
+ reply without content as `c.body(null, status)` (and `c.redirect()` for a
266
+ 3xx other than 304), a binary or streamed body as `unknown`. A status Hono
267
+ has no type for is typed `any`, and an operation that declares only
268
+ `default` or ranges replies `Response`.
269
+
270
+ #### `HonoSpec`
271
+
272
+ ```ts
273
+ interface HonoSpec {
274
+ operations: Operations;
275
+ replies: Replies;
276
+ routes: OperationsByRoute;
277
+ paths: PathsByMethod;
278
+ tags: OperationsByTag;
279
+ tagPaths: PathsByTag;
280
+ }
281
+ ```
282
+
283
+ The spec, as this package reads it: the [`ApiSpec`](#apispec) that every
284
+ generic below is given. The indexes come from the generated `types.ts`.
285
+
286
+ ### Functions
287
+
288
+ #### `createApi()`
289
+
290
+ ```ts
291
+ function createApi<S extends ApiSpec>(
292
+ operations: OperationTable,
293
+ defaults?: ApiOptions,
294
+ ): Api<S>;
295
+ ```
296
+
297
+ The engine: one registry over the `operations` table of `operations.ts`.
298
+ `defaults` apply to every `routes()` it makes, under the options given
299
+ there. Returns an [`Api`](#api-1). `hono.ts` calls it; an app calls the
300
+ generated `createApi()` instead.
301
+
302
+ #### `validationErrorHandler()`
303
+
304
+ ```ts
305
+ const validationErrorHandler: (failure: ValidationFailure, c: Context) => Response;
306
+ ```
307
+
308
+ The default answer to a failure. For a `request`, a 400:
309
+ `{ status: 400, message: 'errors.validation-failed', timestamp, issues }`.
310
+ For a `response`, a 500 without the issues, which would show what the reply
311
+ held: `{ status: 500, message: 'errors.response-validation-failed', timestamp }`,
312
+ the issues going to `console.error`. Call it from your own hook to fall back
313
+ to it.
314
+
315
+ #### `streamEvents()`
316
+
317
+ ```ts
318
+ function streamEvents<Event>(
319
+ c: Context,
320
+ id: string,
321
+ write: (stream: EventWriter<Event>) => Promise<void>,
322
+ ): Response;
323
+ ```
324
+
325
+ Replies with server-sent events from the handler of operation `id`, as
326
+ `text/event-stream` with `cache-control: no-cache`, under the operation's
327
+ first 2xx status that declares them. The reply ends when `write` returns;
328
+ a throw inside it ends it too and goes to `console.error`. Throws when `id`
329
+ is not the route running `c`, or when the operation does not reply with
330
+ server-sent events. See [Streams](#streams).
331
+
332
+ #### `streamLines()`
333
+
334
+ ```ts
335
+ function streamLines<Item>(
336
+ c: Context,
337
+ id: string,
338
+ write: (stream: LineWriter<Item>) => Promise<void>,
339
+ ): Response;
340
+ ```
341
+
342
+ Replies with JSON lines from the handler of operation `id`: one JSON text a
343
+ line, as the media type the spec declares (`application/jsonl`,
344
+ `application/x-ndjson`, or `application/json-seq`, which puts a record
345
+ separator before each). Ends and throws as [`streamEvents()`](#streamevents-1).
346
+
347
+ ### Objects
348
+
349
+ #### `Api`
350
+
351
+ ```ts
352
+ interface Api<S extends ApiSpec> {
353
+ routes(app, options?): Routes;
354
+ missing(tag?): string[];
355
+ assertComplete(tag?): void;
356
+ }
357
+ ```
358
+
359
+ What `createApi()` returns: one registry, shared by every `routes()` it
360
+ makes. See [Modules](#modules).
361
+
362
+ ##### `api.routes()`
363
+
364
+ ```ts
365
+ routes<Prefix extends string = '', Tag extends keyof S['tags'] & string = never>(
366
+ app: Hono<any, any, any>,
367
+ options?: RoutesOptions<Prefix, Tag>,
368
+ ): Routes<S, ScopeOf<S, Tag>, Prefix>;
369
+ ```
370
+
371
+ Registers routes on `app`, which may be a module's sub-app. The options
372
+ override the defaults given to `createApi()`.
373
+
374
+ | Option | Type | Default | Description |
375
+ | --- | --- | --- | --- |
376
+ | `prefix` | `string` | none | Where `app` is mounted, as the spec writes it: `/employees`. Routes are registered relative to it, and only paths under it are offered. |
377
+ | `tag` | a tag of the spec | none | Offers only the operations with this tag. |
378
+ | `onValidationError` | [`ValidationErrorHook`](#validationerrorhook) | `validationErrorHandler` | Answers a failure. |
379
+ | `validateResponses` | `boolean` | `false` | Checks every reply against the spec. |
380
+
381
+ ##### `api.missing()`
382
+
383
+ ```ts
384
+ missing(tag?: keyof S['tags'] & string): string[];
385
+ ```
386
+
387
+ The `operationId`s with no route yet, of one tag or of the whole spec. An
388
+ operation Hono cannot route is never missing.
389
+
390
+ ##### `api.assertComplete()`
391
+
392
+ ```ts
393
+ assertComplete(tag?: keyof S['tags'] & string): void;
394
+ ```
395
+
396
+ Throws an `Error` listing every operation of `missing(tag)`, as
397
+ `operationId (METHOD /path)`; returns when there is none. Call it once every
398
+ module is registered.
399
+
400
+ #### `Routes`
401
+
402
+ ```ts
403
+ type Routes<S extends ApiSpec, Sc extends Scope = Whole<S>, Prefix extends string = ''>;
404
+ ```
405
+
406
+ What `createRoutes()` and `api.routes()` return: a method per HTTP method,
407
+ `operation`, `validate` and `with`. Every registration returns the same
408
+ `Routes`, so calls chain. See [Registering routes](#registering-routes).
409
+
410
+ ##### `routes.get()`, `routes.put()`, `routes.post()`, `routes.delete()`, `routes.options()`, `routes.head()`, `routes.patch()`, `routes.trace()`, `routes.query()`
411
+
412
+ ```ts
413
+ get<P extends /* a GET path of the scope, starting with the prefix */>(
414
+ path: P,
415
+ ...chain: [...MiddlewareHandler[], RouteHandler<S, /* the operationId of GET P */>]
416
+ ): Routes<S, Sc, Prefix>;
417
+ ```
418
+
419
+ Registers the operation at `path`, written as the spec writes it
420
+ (`/employees/{id}`), with middlewares then its handler. The handler's
421
+ `c.req.valid()` and replies are those of the operation. Throws at
422
+ registration in the cases listed under [Modules](#modules); `head()` always
423
+ throws, since Hono answers `HEAD` with the `GET` route.
424
+
425
+ ##### `routes.operation()`
426
+
427
+ ```ts
428
+ operation<Id extends Sc['ids']>(id: Id, ...chain: Chain<S, Id>): Routes<S, Sc, Prefix>;
429
+ ```
430
+
431
+ Registers an operation by its `operationId` instead of its path. Throws as
432
+ the methods above.
433
+
434
+ ##### `routes.validate`
435
+
436
+ ```ts
437
+ readonly validate: MiddlewareHandler;
438
+ ```
439
+
440
+ A marker for where the request is validated in a chain, when a middleware
441
+ needs validated input: `routes.put(path, auth, routes.validate, owns, handler)`.
442
+ Without it, validation runs after every middleware. It throws if it is ever
443
+ run outside a `routes` chain.
444
+
445
+ ##### `routes.with()`
446
+
447
+ ```ts
448
+ with(options: ApiOptions): Routes<S, Sc, Prefix>;
449
+ ```
450
+
451
+ The same routes, on the same app and registry, with `onValidationError` or
452
+ `validateResponses` changed for the routes registered through it only.
453
+
454
+ ### Types
455
+
456
+ #### `ApiOptions`
457
+
458
+ | Field | Type | Description |
459
+ | --- | --- | --- |
460
+ | `onValidationError?` | [`ValidationErrorHook`](#validationerrorhook) | Answers a request the spec refuses, or a reply it does not declare. Default `validationErrorHandler`. |
461
+ | `validateResponses?` | `boolean` | Checks every reply: its status, its content type and, for JSON and text, its body. For development and tests. |
462
+
463
+ Taken by `createApi()` and `routes.with()`. See
464
+ [Validation errors](#validation-errors) and [Checking replies](#checking-replies).
465
+
466
+ #### `RoutesOptions`
467
+
468
+ ```ts
469
+ interface RoutesOptions<Prefix extends string = '', Tag extends string = never> extends ApiOptions {
470
+ prefix?: Prefix;
471
+ tag?: Tag;
472
+ }
473
+ ```
474
+
475
+ Taken by `createRoutes()` and `api.routes()`; see the table under
476
+ [`api.routes()`](#apiroutes).
477
+
478
+ #### `ValidationFailure`
479
+
480
+ | Field | Type | Description |
481
+ | --- | --- | --- |
482
+ | `kind` | `'request' \| 'response'` | A request the spec refuses, or, with `validateResponses`, a reply it does not declare. |
483
+ | `operationId` | `string` | The operation. |
484
+ | `method` | `string` | Its method, lowercase. |
485
+ | `path` | `string` | As the spec writes it: `/employees/{id}`. |
486
+ | `status?` | `number` | The reply's status, for a `response` failure. |
487
+ | `issues` | [`ValidationIssue[]`](#validationissue) | Every issue, from every target at once. |
488
+
489
+ What `onValidationError` receives.
490
+
491
+ #### `ValidationIssue`
492
+
493
+ | Field | Type | Description |
494
+ | --- | --- | --- |
495
+ | `target` | [`ValidationTarget`](#validationtarget) `\| 'response'` | Where the value was read. |
496
+ | `path` | `(string \| number)[]` | Inside the target: `['items', 0, 'name']`, or `[]` for the whole of it. |
497
+ | `code` | `string` | Zod's issue code, or one of `invalid_json`, `invalid_form`, `invalid_content_type`, `missing_body`, `repeated_parameter`, `undeclared_status`. |
498
+ | `message` | `string` | What is wrong. |
499
+
500
+ One entry of `ValidationFailure.issues`, and of the default 400's `issues`.
501
+
502
+ #### `ValidationTarget`
503
+
504
+ ```ts
505
+ type ValidationTarget = 'param' | 'query' | 'header' | 'json' | 'form' | 'body';
506
+ ```
507
+
508
+ Where a request value was read: a target of `c.req.valid()`, or `body` for
509
+ a text body, which a handler reads with `c.req.text()`.
510
+
511
+ #### `ValidationErrorHook`
512
+
513
+ ```ts
514
+ type ValidationErrorHook = (
515
+ failure: ValidationFailure,
516
+ c: Context,
517
+ ) => Response | undefined | Promise<Response | undefined>;
518
+ ```
519
+
520
+ The type of `onValidationError`: return a `Response` to send it, throw to
521
+ hand the failure to `app.onError`, or return nothing for the default.
522
+
523
+ #### `SchemaIssue`
524
+
525
+ ```ts
526
+ interface SchemaIssue {
527
+ readonly path: readonly PropertyKey[];
528
+ readonly code: string;
529
+ readonly message: string;
530
+ }
531
+ ```
532
+
533
+ What the engine reads of a Zod issue, in a [`Validator`](#validator)'s
534
+ failed result.
535
+
536
+ #### `EventWriter`
537
+
538
+ | Field | Type | Description |
539
+ | --- | --- | --- |
540
+ | `write(event)` | `(event: Event) => Promise<void>` | Sends an event, `{ event?, data, id?, retry? }` in `hono.ts`. Throws for an event name the spec does not declare, or an `event` or `id` holding a line break. |
541
+ | `sleep(ms)` | `(ms: number) => Promise<void>` | Resolves after `ms` milliseconds: a pause between two items. |
542
+ | `aborted` | `boolean` | Whether the client went away: stop writing then. |
543
+ | `onAbort(listener)` | `(listener: () => void \| Promise<void>) => void` | Runs `listener` when the client goes away. |
544
+
545
+ The `stream` that `streamEvents()` hands to `write`.
546
+
547
+ #### `LineWriter`
548
+
549
+ | Field | Type | Description |
550
+ | --- | --- | --- |
551
+ | `write(item)` | `(item: Item) => Promise<void>` | Sends an item, as a line of JSON. |
552
+ | `sleep(ms)`, `aborted`, `onAbort(listener)` | | As on [`EventWriter`](#eventwriter). |
553
+
554
+ The `stream` that `streamLines()` hands to `write`.
555
+
556
+ #### `RouteHandler`
557
+
558
+ ```ts
559
+ type RouteHandler<S extends ApiSpec, Id extends string> = (
560
+ c: Context</* … */>, // c.req.valid() holds the operation's validated input
561
+ next: Next,
562
+ ) => Reply | Promise<Reply>; // S['replies'][Id]
563
+ ```
564
+
565
+ The handler of operation `Id`: the last argument of a registration.
566
+
567
+ #### `Chain`
568
+
569
+ ```ts
570
+ type Chain<S extends ApiSpec, Id extends string> = [...MiddlewareHandler[], RouteHandler<S, Id>];
571
+ ```
572
+
573
+ Middlewares, then the handler: the arguments after the path or the
574
+ `operationId`.
575
+
576
+ #### `Method`
577
+
578
+ ```ts
579
+ type Method = 'get' | 'put' | 'post' | 'delete' | 'options' | 'head' | 'patch' | 'trace' | 'query';
580
+ ```
581
+
582
+ The HTTP methods of an operation, and the registration methods of
583
+ [`Routes`](#routes).
584
+
585
+ #### `ApiSpec`
586
+
587
+ | Field | Type | Description |
588
+ | --- | --- | --- |
589
+ | `operations` | `object` | `Operations`, from `types.ts`, keyed by `operationId`. |
590
+ | `replies` | `object` | `Replies`, from `hono.ts`, keyed by `operationId`. |
591
+ | `routes` | `object` | `OperationsByRoute`: `'put /employees/{id}'` to its `operationId`. |
592
+ | `paths` | `{ [M in Method]: string }` | `PathsByMethod`. |
593
+ | `tags` | `object` | `OperationsByTag`. |
594
+ | `tagPaths` | `object` | `PathsByTag`. |
595
+
596
+ What a spec must provide to type `routes`; the generated
597
+ [`HonoSpec`](#honospec) is one.
598
+
599
+ #### `Scope`
600
+
601
+ ```ts
602
+ interface Scope {
603
+ ids: string;
604
+ paths: { [M in Method]: string };
605
+ }
606
+ ```
607
+
608
+ The operations a `Routes` offers: their `operationId`s, and their paths by
609
+ method.
610
+
611
+ #### `Whole`
612
+
613
+ The scope of every operation of a spec: `Whole<HonoSpec>`, the default of
614
+ `Routes`.
615
+
616
+ #### `Tagged`
617
+
618
+ The scope of the operations of one tag: `Tagged<HonoSpec, 'employees'>`.
619
+
620
+ #### `ScopeOf`
621
+
622
+ `Whole<S>` when no tag is given, else `Tagged<S, Tag>`; evaluated once per
623
+ `routes()`: `ScopeOf<HonoSpec, 'employees'>`.
624
+
625
+ #### `OperationTable`
626
+
627
+ ```ts
628
+ type OperationTable = { readonly [operationId: string]: RuntimeOperation };
629
+ ```
630
+
631
+ The `operations` table that `operations.ts` exports, and `createApi()` takes.
632
+
633
+ #### `RuntimeOperation`
634
+
635
+ | Field | Type | Description |
636
+ | --- | --- | --- |
637
+ | `method` | [`Method`](#method) | The operation's method. |
638
+ | `path` | `string` | As the spec writes it: `/employees/{id}`. |
639
+ | `honoPath` | `string` | As Hono routes it: `/employees/:id`. |
640
+ | `tags` | `readonly string[]` | Its tags. |
641
+ | `parameters` | `readonly { name; in: 'path' \| 'query' \| 'header'; list: boolean; explode: boolean }[]` | How each parameter is read. |
642
+ | `param`, `query`, `header` | [`Validator`](#validator) | The validator of each target. |
643
+ | `body?` | `{ required: boolean; content: { [mediaType]: RuntimeMedia } }` | Its request body, by media type. |
644
+ | `responses` | `{ [status]: { [mediaType]: RuntimeMedia } }` | Its replies, by status, then media type. |
645
+
646
+ One entry of the [`OperationTable`](#operationtable).
647
+
648
+ #### `RuntimeMedia`
649
+
650
+ | Field | Type | Description |
651
+ | --- | --- | --- |
652
+ | `kind` | `'json' \| 'form' \| 'text' \| 'binary' \| 'sse' \| 'jsonl'` | How the content is read or written. |
653
+ | `schema?` | [`Validator`](#validator) | Validates the whole content. |
654
+ | `events?` | `{ [event]: Validator \| null }` | `sse`: each event's data, by name: a validator for JSON, `null` for text. |
655
+ | `item?` | [`Validator`](#validator) | `jsonl`: each item. |
656
+
657
+ One media type of a body or a reply in a [`RuntimeOperation`](#runtimeoperation).
658
+
659
+ #### `Validator`
660
+
661
+ ```ts
662
+ interface Validator {
663
+ safeParse(value: unknown):
664
+ | { success: true; data: unknown }
665
+ | { success: false; error: { issues: readonly SchemaIssue[] } };
666
+ }
667
+ ```
668
+
669
+ What the engine asks of a validator: Zod's `safeParse`. Every validator of
670
+ the table is one.
671
+
672
+ ## Traps
673
+
674
+ - **Give `c.json()` a status.** Without one, Hono types the reply with any
675
+ contentful status, and it matches no declared reply.
676
+ - **Reply with plain objects.** A Mongoose document does not type as its
677
+ schema; return `.lean()` results.
678
+ - **Read the body through `c.req`, never `c.req.raw`.** A middleware that
679
+ drains `c.req.raw` leaves the validator nothing to read.
680
+ - **Register the static path first.** `/users/{id}` registered before
681
+ `/users/me` would answer for it, so the second registration throws.
682
+ - **`security` is not enforced.** Register your own auth middlewares.
683
+ - **Limit the body size yourself.** Put Hono's `bodyLimit` first.
684
+
685
+ ## Documentation
686
+
687
+ - [Typed Hono routes](docs/guide.md): how a request is read, every issue
688
+ code, reply checks, modules, the mistakes caught at startup, and the
689
+ traps.
690
+ - [The architecture](docs/architecture.md): registration, validation, the
691
+ route types, what they cost, and the tests. Read it before working on the
692
+ runtime.