@mpen/routekit 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 +526 -0
- package/dist/bin.mjs +574 -0
- package/package.json +95 -0
- package/src/bin/gen-api-client.test.ts +70 -0
- package/src/bin/gen-api-client.ts +986 -0
- package/src/client/headers.ts +31 -0
- package/src/client/index.ts +8 -0
- package/src/client/promise.ts +11 -0
- package/src/client/react/index.test.tsx +266 -0
- package/src/client/react/index.ts +431 -0
- package/src/client/responses.test.ts +151 -0
- package/src/client/responses.ts +278 -0
- package/src/client/transport.ts +74 -0
- package/src/client/transports/body-codec.ts +61 -0
- package/src/client/transports/fetch.ts +113 -0
- package/src/client/tsconfig.json +9 -0
- package/src/client/types.ts +15 -0
- package/src/client/url.ts +31 -0
- package/src/index.ts +63 -0
- package/src/router/fetch-types.ts +13 -0
- package/src/router/handlers/index.ts +2 -0
- package/src/router/handlers/openapi/index.ts +2 -0
- package/src/router/handlers/openapi/openapi.ts +293 -0
- package/src/router/integration/zod-openapi.test.ts +74 -0
- package/src/router/lib/charset.test.ts +22 -0
- package/src/router/lib/charset.ts +133 -0
- package/src/router/lib/collections.ts +3 -0
- package/src/router/lib/format.test.ts +67 -0
- package/src/router/lib/format.ts +35 -0
- package/src/router/lib/host.ts +4 -0
- package/src/router/lib/json-schema.ts +6 -0
- package/src/router/lib/media-type.test.ts +122 -0
- package/src/router/lib/media-type.ts +289 -0
- package/src/router/lib/pathname.test.ts +18 -0
- package/src/router/lib/pathname.ts +19 -0
- package/src/router/lib/route-names.ts +70 -0
- package/src/router/lib/route-normalize.test.ts +36 -0
- package/src/router/lib/route-normalize.ts +67 -0
- package/src/router/lib/schema-merge.ts +56 -0
- package/src/router/middleware/accept-ctx.test.ts +33 -0
- package/src/router/middleware/accept-ctx.ts +12 -0
- package/src/router/middleware/body-limit.test.ts +112 -0
- package/src/router/middleware/body-limit.ts +121 -0
- package/src/router/middleware/content-type-context.ts +0 -0
- package/src/router/middleware/cors.test.ts +269 -0
- package/src/router/middleware/cors.ts +490 -0
- package/src/router/middleware/csrf.test.ts +106 -0
- package/src/router/middleware/csrf.ts +192 -0
- package/src/router/middleware/define.ts +249 -0
- package/src/router/middleware/index.ts +34 -0
- package/src/router/middleware/jsxhtml-response.ts +0 -0
- package/src/router/middleware/oas-swagger.ts +0 -0
- package/src/router/middleware/rate-limit.test.ts +886 -0
- package/src/router/middleware/rate-limit.ts +920 -0
- package/src/router/middleware/request-id-ctx.test.ts +183 -0
- package/src/router/middleware/request-id-ctx.ts +135 -0
- package/src/router/middleware/request-logger-format.test.ts +16 -0
- package/src/router/middleware/request-logger-format.ts +269 -0
- package/src/router/middleware/request-logger.test.ts +267 -0
- package/src/router/middleware/request-logger.ts +131 -0
- package/src/router/middleware/start-time-ctx.ts +5 -0
- package/src/router/request.ts +611 -0
- package/src/router/response/core.ts +181 -0
- package/src/router/response/directives.ts +233 -0
- package/src/router/response/formats/content/bodyless.ts +54 -0
- package/src/router/response/formats/content/content.ts +79 -0
- package/src/router/response/formats/content/index.ts +2 -0
- package/src/router/response/formats/json-rpc/index.ts +2 -0
- package/src/router/response/formats/problem/badRequest.ts +90 -0
- package/src/router/response/formats/problem/conflict.ts +90 -0
- package/src/router/response/formats/problem/created.ts +40 -0
- package/src/router/response/formats/problem/index.ts +27 -0
- package/src/router/response/formats/problem/notFound.ts +90 -0
- package/src/router/response/formats/problem/permissionDenied.ts +90 -0
- package/src/router/response/formats/problem/problem.test.ts +888 -0
- package/src/router/response/formats/problem/rateLimited.ts +90 -0
- package/src/router/response/formats/problem/responses.ts +219 -0
- package/src/router/response/formats/problem/root-errors.ts +48 -0
- package/src/router/response/formats/problem/sessionExpired.ts +90 -0
- package/src/router/response/formats/problem/types.ts +170 -0
- package/src/router/response/formats/problem/unauthenticated.ts +90 -0
- package/src/router/response/formats/problem/valibot.ts +410 -0
- package/src/router/response/formats/status/index.ts +1 -0
- package/src/router/response/formats/status/responses.ts +59 -0
- package/src/router/response/formats/status/status.test.ts +21 -0
- package/src/router/response/framers.ts +85 -0
- package/src/router/response/index.ts +28 -0
- package/src/router/response/openapi.test.ts +96 -0
- package/src/router/response/openapi.ts +1 -0
- package/src/router/response/serializers.ts +66 -0
- package/src/router/response/stream.ts +35 -0
- package/src/router/router.test.ts +1571 -0
- package/src/router/router.ts +1965 -0
- package/src/router/routes/index.ts +46 -0
- package/src/router/routes/valibot/index.ts +18 -0
- package/src/router/routes/valibot/valibot.ts +1393 -0
- package/src/router/routes/valibot.test.ts +286 -0
- package/src/router/routes/zod/index.ts +18 -0
- package/src/router/routes/zod/zod.ts +1318 -0
- package/src/router/routes/zod.test.ts +280 -0
- package/src/router/server-interface.ts +31 -0
- package/src/router/types.ts +657 -0
package/README.md
ADDED
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
# @mpen/routekit
|
|
2
|
+
|
|
3
|
+
Typed server-side routing utilities for Fetch-compatible runtimes.
|
|
4
|
+
|
|
5
|
+
`routekit` is a small router around the platform `Request`/`Response` APIs. It matches
|
|
6
|
+
`URLPattern` routes, runs typed middleware, supports schema-backed handlers with Zod or
|
|
7
|
+
Valibot, can expose route metadata as OpenAPI, and can generate a typed API client from
|
|
8
|
+
the same router definitions.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
bun add @mpen/routekit
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Install the schema library you plan to use:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
bun add zod
|
|
20
|
+
# or
|
|
21
|
+
bun add valibot @valibot/to-json-schema
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { Router, ok } from '@mpen/routekit'
|
|
28
|
+
|
|
29
|
+
const router = new Router()
|
|
30
|
+
|
|
31
|
+
router.get('/', () => ok({ message: 'Hello World!' }))
|
|
32
|
+
|
|
33
|
+
router.get('/users/:id', ({ path }) => {
|
|
34
|
+
const { id } = path as { id: string }
|
|
35
|
+
return ok({ id })
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
export default router
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Use the router anywhere a Fetch-compatible handler is accepted:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import router from './router'
|
|
45
|
+
|
|
46
|
+
Bun.serve({
|
|
47
|
+
port: 3000,
|
|
48
|
+
fetch: router.fetch,
|
|
49
|
+
})
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
You can also exercise a router directly in tests:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
const response = await router.fetch(new Request('https://example.com/users/123'))
|
|
56
|
+
expect(await response.json()).toEqual({ id: '123' })
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Routing
|
|
60
|
+
|
|
61
|
+
Routes can be registered with method helpers:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
router.get('/health', () => text('ok'))
|
|
65
|
+
router.head('/health', () => noContent())
|
|
66
|
+
router.post('/items', async ({ req }) => ok(await req.json()))
|
|
67
|
+
router.put('/items/:id', () => text('updated'))
|
|
68
|
+
router.patch('/items/:id', () => text('patched'))
|
|
69
|
+
router.delete('/items/:id', () => text('deleted'))
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Or with a full route definition:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import { HttpMethod } from '@mpen/http'
|
|
76
|
+
|
|
77
|
+
router.add({
|
|
78
|
+
name: 'items.detail',
|
|
79
|
+
method: HttpMethod.GET,
|
|
80
|
+
path: '/items/:id',
|
|
81
|
+
accept: 'application/json',
|
|
82
|
+
meta: {
|
|
83
|
+
openapi: {
|
|
84
|
+
summary: 'Fetch an item',
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
handler: ({ path }) => ok({ id: (path as { id: string }).id }),
|
|
88
|
+
})
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`path` may be a string or a `URLPattern`. Named path parameters are exposed on
|
|
92
|
+
`ctx.path`. When a route name is omitted, routekit derives one from the method and
|
|
93
|
+
path so tooling such as API client generation still has a stable name to work with.
|
|
94
|
+
|
|
95
|
+
Routers can be mounted under a prefix:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
const api = new Router()
|
|
99
|
+
api.get('/health', () => new Response('ok'))
|
|
100
|
+
|
|
101
|
+
const app = new Router()
|
|
102
|
+
app.mount('/api', api)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Handler Results
|
|
106
|
+
|
|
107
|
+
Handlers may return:
|
|
108
|
+
|
|
109
|
+
- a `Response`
|
|
110
|
+
- a `RoutekitResponse` from helpers such as `ok()`, `response()`, `text()`, or `html()`
|
|
111
|
+
- a `string`, `Uint8Array`, `Buffer`, or `ReadableStream` for raw native bodies
|
|
112
|
+
- a structured object wrapped with `ok()` for content negotiation
|
|
113
|
+
- an async generator that yields typed response directives
|
|
114
|
+
|
|
115
|
+
For structured responses, return `ok(value)`. When no `Content-Type` is set, the router
|
|
116
|
+
serializes the body using the request's `Accept` header:
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
router.get('/profile', () => ok({ name: 'Ada' }))
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Use `text()` and `html()` for represented bodies that should skip negotiation:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
router.get('/health', () => text('ok'))
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Streaming handlers yield explicit directives:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
router.get('/events', async function* () {
|
|
132
|
+
yield head(HttpStatus.OK, { 'content-type': 'text/plain; charset=utf-8' })
|
|
133
|
+
yield chunk('hello ')
|
|
134
|
+
yield chunk('world')
|
|
135
|
+
})
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Configuration
|
|
139
|
+
|
|
140
|
+
Request body parsers, response body serializers, and loggers are configured with fluent
|
|
141
|
+
router methods. `add...` methods append to inherited configuration, while `set...`
|
|
142
|
+
methods replace inherited configuration for that router subtree.
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
const router = new Router()
|
|
146
|
+
.addRequestBodyParser(customRequestBodyParser)
|
|
147
|
+
.addResponseBodySerializer(customResponseBodySerializer)
|
|
148
|
+
.setLogger(logger)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Use `setRequestBodyParsers()` or `setResponseBodySerializers()` when a subtree should
|
|
152
|
+
have an exact parser or serializer list instead of inheriting from its parent.
|
|
153
|
+
|
|
154
|
+
Parser and serializer `mediaTypes` entries may include an Accept-style `q`
|
|
155
|
+
preference. Client `Accept` quality wins for responses; server `q` values resolve
|
|
156
|
+
ties before registration order.
|
|
157
|
+
|
|
158
|
+
## Middleware
|
|
159
|
+
|
|
160
|
+
Middleware runs in registration order and can add fields to the request context. The
|
|
161
|
+
added fields are reflected in handler types.
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
import type { ContextMiddleware } from '@mpen/routekit'
|
|
165
|
+
|
|
166
|
+
const auth: ContextMiddleware<{ userId: string }> = (ctx) => {
|
|
167
|
+
ctx.userId = 'user-123'
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const router = new Router().use(auth)
|
|
171
|
+
|
|
172
|
+
router.get('/me', ({ userId }) => ok({ userId }))
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
`router.use(middleware)` applies middleware to the router itself, not only to routes
|
|
176
|
+
registered after the call. Middleware registered on a router runs for every matching
|
|
177
|
+
route on that router, including routes that were added before the middleware was
|
|
178
|
+
registered.
|
|
179
|
+
|
|
180
|
+
Use `router.mount()` with inline configuration when middleware should apply to a subset
|
|
181
|
+
of routes:
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
const router = new Router()
|
|
185
|
+
|
|
186
|
+
router.get('/health', () => text('ok'))
|
|
187
|
+
|
|
188
|
+
router.mount({ prefix: '/admin', middleware: auth }, (admin) => {
|
|
189
|
+
admin.get('/users', ({ userId }) => ok({ userId }))
|
|
190
|
+
admin.post('/users', ({ userId }) => ok({ createdBy: userId }))
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
router.get('/status', () => ok({ status: 'up' }))
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
In this example, `auth` runs for `/admin/users` but not for `/health` or `/status`.
|
|
197
|
+
|
|
198
|
+
Middleware that creates a response declares it with a schema-bound factory. Its response
|
|
199
|
+
metadata is inherited automatically by every affected route.
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
import { HttpStatus } from '@mpen/http'
|
|
203
|
+
import { response } from '@mpen/routekit'
|
|
204
|
+
import { defineZodMiddleware } from '@mpen/routekit/routes'
|
|
205
|
+
import { z } from 'zod'
|
|
206
|
+
|
|
207
|
+
const requireAuth = defineZodMiddleware({
|
|
208
|
+
responses: {
|
|
209
|
+
[HttpStatus.UNAUTHORIZED]: z.object({ error: z.literal('unauthorized') }),
|
|
210
|
+
},
|
|
211
|
+
run: (_ctx, { respond }) =>
|
|
212
|
+
respond(response({ error: 'unauthorized' }, { status: HttpStatus.UNAUTHORIZED })),
|
|
213
|
+
})
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Encapsulation and Inheritance
|
|
217
|
+
|
|
218
|
+
`mount()` composes routers from inline route blocks or existing router instances:
|
|
219
|
+
|
|
220
|
+
- `router.mount({ prefix, middleware }, configure)` creates a scoped inline router.
|
|
221
|
+
Use it when routes are declared together and should share extra middleware or config.
|
|
222
|
+
- `router.mount(prefix, childRouter)` attaches an existing router, optionally under a
|
|
223
|
+
path prefix. Use it when routes live in another module or should be reusable as their
|
|
224
|
+
own router.
|
|
225
|
+
- `router.mount({ prefix, middleware }, childRouter)` wraps an existing router with
|
|
226
|
+
additional runtime middleware.
|
|
227
|
+
|
|
228
|
+
Parent middleware runs first, followed by scoped middleware and then child router
|
|
229
|
+
middleware. Request body parsers, response body serializers, loggers, and error handlers
|
|
230
|
+
inherit through the same router tree. Child routers can append parser/serializer config
|
|
231
|
+
or replace it with `setRequestBodyParsers()` and `setResponseBodySerializers()`.
|
|
232
|
+
|
|
233
|
+
> [!IMPORTANT]
|
|
234
|
+
> `router.use(middleware)` is retroactive for that router. If you add a route, create a
|
|
235
|
+
> scoped mount, or mount a child router and later call `router.use(auth)`, `auth` still runs
|
|
236
|
+
> for those earlier entries. To mount a child router without that middleware, keep the
|
|
237
|
+
> parent router middleware-free and apply middleware only to scoped mounts or child routers
|
|
238
|
+
> that need it.
|
|
239
|
+
|
|
240
|
+
A mounted router still keeps its own TypeScript context boundary. Handlers declared
|
|
241
|
+
inside the mounted router are typed from that router's middleware, even though parent
|
|
242
|
+
middleware also runs at runtime. Use inline `mount({ middleware }, router => { ... })`
|
|
243
|
+
when handlers should see scoped middleware context in their TypeScript types.
|
|
244
|
+
|
|
245
|
+
This differs from some other routers:
|
|
246
|
+
|
|
247
|
+
- [Express](https://expressjs.com/en/guide/using-middleware.html) uses an ordered
|
|
248
|
+
middleware stack. Middleware registered before a mounted router can wrap it; middleware
|
|
249
|
+
registered after the mount does not retroactively affect it.
|
|
250
|
+
- [Hono](https://hono.dev/docs/guides/middleware) also runs middleware in registration
|
|
251
|
+
order, and [route grouping](https://hono.dev/docs/api/routing#grouping-ordering) adds
|
|
252
|
+
the child router's stored routes to the parent at the time `route()` is called.
|
|
253
|
+
- [Fastify](https://fastify.dev/docs/latest/Reference/Encapsulation/) is closer to
|
|
254
|
+
RouteKit's model: child contexts can access parent plugins, hooks, and decorators,
|
|
255
|
+
while sibling contexts stay isolated.
|
|
256
|
+
- [Elysia](https://elysiajs.com/tutorial/getting-started/encapsulation/) encapsulates
|
|
257
|
+
hooks to their own instance by default and exposes explicit `local`, `scoped`, and
|
|
258
|
+
`global` scope controls.
|
|
259
|
+
|
|
260
|
+
Middleware can also wrap downstream results:
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
import { defineMiddleware } from '@mpen/routekit'
|
|
264
|
+
|
|
265
|
+
router.use(
|
|
266
|
+
defineMiddleware({
|
|
267
|
+
async run(_ctx, { next, forward }) {
|
|
268
|
+
const response = await next()
|
|
269
|
+
if (response instanceof Response) {
|
|
270
|
+
response.headers.set('x-powered-by', 'routekit')
|
|
271
|
+
}
|
|
272
|
+
return forward(response)
|
|
273
|
+
},
|
|
274
|
+
}),
|
|
275
|
+
)
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Built-in middleware is available from `@mpen/routekit/middleware`:
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
import { TerminalLogger } from '@mpen/logger'
|
|
282
|
+
import {
|
|
283
|
+
acceptCtx,
|
|
284
|
+
bodyLimit,
|
|
285
|
+
cors,
|
|
286
|
+
requestIdCtx,
|
|
287
|
+
requestLogger,
|
|
288
|
+
startTimeCtx,
|
|
289
|
+
} from '@mpen/routekit/middleware'
|
|
290
|
+
|
|
291
|
+
const router = new Router()
|
|
292
|
+
.setLogger(new TerminalLogger())
|
|
293
|
+
.useRequest(requestIdCtx({ writeHeaderName: 'x-request-id' }))
|
|
294
|
+
.useRequest(requestLogger())
|
|
295
|
+
|
|
296
|
+
router.use([
|
|
297
|
+
startTimeCtx(),
|
|
298
|
+
acceptCtx(),
|
|
299
|
+
bodyLimit({ maxSize: 1024 * 1024 }),
|
|
300
|
+
cors({ origin: 'https://app.example.com', credentials: true }),
|
|
301
|
+
])
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Every request context exposes the inherited `ctx.logger`. Request-boundary middleware runs
|
|
305
|
+
for generated responses as well as matched handlers, so `requestIdCtx()` and
|
|
306
|
+
`requestLogger()` correlate status, timing, and known response body-size output consistently.
|
|
307
|
+
It also records `user_agent.original`. Behind a trusted ingress proxy, opt in to the original
|
|
308
|
+
client-address header:
|
|
309
|
+
|
|
310
|
+
```ts
|
|
311
|
+
router.useRequest(requestLogger({ trustedClientAddressHeader: 'x-forwarded-for' }))
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
This records the first forwarded address as `client.address`; only enable it when clients
|
|
315
|
+
cannot send that header directly.
|
|
316
|
+
|
|
317
|
+
`rateLimit()` supports fixed-window identity, subnet, ASN, country, and endpoint limits.
|
|
318
|
+
It can use the default in-memory storage or a custom `RateLimitStorage` implementation.
|
|
319
|
+
|
|
320
|
+
## Zod Routes
|
|
321
|
+
|
|
322
|
+
Zod helpers validate request input, infer typed `params`, and attach JSON Schema metadata
|
|
323
|
+
to routes for OpenAPI and client generation.
|
|
324
|
+
|
|
325
|
+
```ts
|
|
326
|
+
import { HttpStatus } from '@mpen/http'
|
|
327
|
+
import { Router, ok } from '@mpen/routekit'
|
|
328
|
+
import { createZodRouteBuilder } from '@mpen/routekit/routes'
|
|
329
|
+
import { z } from 'zod'
|
|
330
|
+
|
|
331
|
+
const router = new Router()
|
|
332
|
+
const route = createZodRouteBuilder()
|
|
333
|
+
|
|
334
|
+
router.post(
|
|
335
|
+
'/books/:id',
|
|
336
|
+
route({
|
|
337
|
+
name: 'books.byId',
|
|
338
|
+
schema: {
|
|
339
|
+
request: {
|
|
340
|
+
path: z.object({ id: z.coerce.number().int() }),
|
|
341
|
+
body: z.object({
|
|
342
|
+
title: z.string(),
|
|
343
|
+
author: z.string(),
|
|
344
|
+
}),
|
|
345
|
+
},
|
|
346
|
+
response: {
|
|
347
|
+
body: {
|
|
348
|
+
[HttpStatus.OK]: z.object({
|
|
349
|
+
id: z.number().int(),
|
|
350
|
+
title: z.string(),
|
|
351
|
+
author: z.string(),
|
|
352
|
+
}),
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
handler: ({ params }) =>
|
|
357
|
+
ok({
|
|
358
|
+
id: params.path.id,
|
|
359
|
+
title: params.body.title,
|
|
360
|
+
author: params.body.author,
|
|
361
|
+
}),
|
|
362
|
+
}),
|
|
363
|
+
)
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
Available Zod APIs:
|
|
367
|
+
|
|
368
|
+
- `createZodRouteBuilder(defaults)` creates method-helper options or full routes with shared defaults.
|
|
369
|
+
- `zodSchemaMiddleware(options)` defines an explicit request/response schema boundary.
|
|
370
|
+
- `defineZodMiddleware(options)` declares and validates terminal responses originated by middleware.
|
|
371
|
+
|
|
372
|
+
Request validation failures return a `400` JSON response by default. Override
|
|
373
|
+
`onRequestValidationError` together with `validationResponses` to customize that response.
|
|
374
|
+
Response validation can be controlled with `validateResponse`.
|
|
375
|
+
|
|
376
|
+
## Valibot Routes
|
|
377
|
+
|
|
378
|
+
Valibot helpers expose the same shape as the Zod helpers:
|
|
379
|
+
|
|
380
|
+
```ts
|
|
381
|
+
import { HttpStatus } from '@mpen/http'
|
|
382
|
+
import { ok } from '@mpen/routekit'
|
|
383
|
+
import { createValibotRouteBuilder } from '@mpen/routekit/routes'
|
|
384
|
+
import * as v from 'valibot'
|
|
385
|
+
|
|
386
|
+
const route = createValibotRouteBuilder()
|
|
387
|
+
|
|
388
|
+
router.post(
|
|
389
|
+
'/books/:id',
|
|
390
|
+
route({
|
|
391
|
+
name: 'books.byId',
|
|
392
|
+
schema: {
|
|
393
|
+
request: {
|
|
394
|
+
path: v.object({
|
|
395
|
+
id: v.pipe(
|
|
396
|
+
v.string(),
|
|
397
|
+
v.transform((value) => Number(value)),
|
|
398
|
+
v.integer(),
|
|
399
|
+
),
|
|
400
|
+
}),
|
|
401
|
+
body: v.object({
|
|
402
|
+
title: v.string(),
|
|
403
|
+
author: v.string(),
|
|
404
|
+
}),
|
|
405
|
+
},
|
|
406
|
+
response: {
|
|
407
|
+
body: {
|
|
408
|
+
[HttpStatus.OK]: v.object({
|
|
409
|
+
id: v.number(),
|
|
410
|
+
title: v.string(),
|
|
411
|
+
author: v.string(),
|
|
412
|
+
}),
|
|
413
|
+
},
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
handler: ({ params }) =>
|
|
417
|
+
ok({
|
|
418
|
+
id: params.path.id,
|
|
419
|
+
title: params.body.title,
|
|
420
|
+
author: params.body.author,
|
|
421
|
+
}),
|
|
422
|
+
}),
|
|
423
|
+
)
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
Available Valibot APIs:
|
|
427
|
+
|
|
428
|
+
- `createValibotRouteBuilder(defaults)`
|
|
429
|
+
- `valibotSchemaMiddleware(options)`
|
|
430
|
+
- `defineValibotMiddleware(options)`
|
|
431
|
+
|
|
432
|
+
## OpenAPI
|
|
433
|
+
|
|
434
|
+
The `openapi()` handler reflects the active router's registered routes and schema metadata.
|
|
435
|
+
|
|
436
|
+
```ts
|
|
437
|
+
import { openapi } from '@mpen/routekit/handlers'
|
|
438
|
+
|
|
439
|
+
router.get(
|
|
440
|
+
'/openapi.json',
|
|
441
|
+
openapi({
|
|
442
|
+
info: {
|
|
443
|
+
title: 'Example API',
|
|
444
|
+
version: '1.0.0',
|
|
445
|
+
},
|
|
446
|
+
servers: [{ url: 'https://api.example.com' }],
|
|
447
|
+
}),
|
|
448
|
+
)
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
Route `meta.openapi` is merged into the generated operation, so route-level summaries,
|
|
452
|
+
tags, security, and custom responses can be supplied beside the handler.
|
|
453
|
+
|
|
454
|
+
## Generated API Clients
|
|
455
|
+
|
|
456
|
+
`routekit-gen-api-client` loads a router module, reads `router.getRoutes()`, and writes a
|
|
457
|
+
typed client from each route's name, method, path, and JSON Schema metadata.
|
|
458
|
+
|
|
459
|
+
```bash
|
|
460
|
+
bun run routekit-gen-api-client ./src/server/router.ts -o ./src/client/api-client.gen.ts -p
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
The router module must export a router instance as `default`, `router`, or another named
|
|
464
|
+
export with a `getRoutes()` method.
|
|
465
|
+
|
|
466
|
+
Generated clients use `@mpen/routekit/client`:
|
|
467
|
+
|
|
468
|
+
```ts
|
|
469
|
+
import { FetchTransport } from '@mpen/routekit/client'
|
|
470
|
+
import { ApiClient } from './api-client.gen'
|
|
471
|
+
|
|
472
|
+
const client = new ApiClient(
|
|
473
|
+
new FetchTransport({
|
|
474
|
+
baseUrl: 'https://api.example.com',
|
|
475
|
+
headers: () => ({ authorization: `Bearer ${token}` }),
|
|
476
|
+
}),
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
const response = await client.books.byId.post({
|
|
480
|
+
path: 123,
|
|
481
|
+
body: { title: 'Dune', author: 'Frank Herbert' },
|
|
482
|
+
headers: { 'content-type': 'application/json' },
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
if (response.ok) {
|
|
486
|
+
const book = await response.parseBody()
|
|
487
|
+
console.log(book.title)
|
|
488
|
+
}
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
Routes with multiple documented response statuses generate a response union narrowed by
|
|
492
|
+
`response.status`:
|
|
493
|
+
|
|
494
|
+
```ts
|
|
495
|
+
const response = await client.widgets.byId.post(options)
|
|
496
|
+
|
|
497
|
+
if (response.status === 400) {
|
|
498
|
+
const body = await response.parseBody()
|
|
499
|
+
console.log(body.message)
|
|
500
|
+
}
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
Use `--client-name <Name>` to change the generated class name, `--import-type
|
|
504
|
+
<Type:module>` for external schema-generated types, and `--response-type <Type>` to use a
|
|
505
|
+
custom generic response wrapper.
|
|
506
|
+
|
|
507
|
+
## Exports
|
|
508
|
+
|
|
509
|
+
- `@mpen/routekit` exports `Router`, response helpers, and core router types.
|
|
510
|
+
- `@mpen/routekit/routes` exports the Zod and Valibot route helpers.
|
|
511
|
+
- `@mpen/routekit/middleware` exports built-in middleware.
|
|
512
|
+
- `@mpen/routekit/handlers` exports `openapi()`.
|
|
513
|
+
- `@mpen/routekit/client` exports generated-client transports, response wrappers, body codecs, and URL/header helpers.
|
|
514
|
+
|
|
515
|
+
## Development
|
|
516
|
+
|
|
517
|
+
From this repository:
|
|
518
|
+
|
|
519
|
+
```bash
|
|
520
|
+
bun run --cwd packages/routekit build
|
|
521
|
+
bun test packages/routekit
|
|
522
|
+
bun run --cwd packages/routekit gen
|
|
523
|
+
bun run --cwd packages/routekit gen3
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
The generated example clients live under `packages/routekit/examples`.
|