@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 +692 -0
- package/dist/engine.d.ts +75 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/errors.d.ts +50 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +545 -0
- package/dist/index.js.map +13 -0
- package/dist/routable.d.ts +10 -0
- package/dist/routable.d.ts.map +1 -0
- package/dist/streams.d.ts +40 -0
- package/dist/streams.d.ts.map +1 -0
- package/dist/types.d.ts +103 -0
- package/dist/types.d.ts.map +1 -0
- package/docs/architecture.md +166 -0
- package/docs/guide.md +324 -0
- package/package.json +47 -0
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.
|