@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.
Files changed (102) hide show
  1. package/README.md +526 -0
  2. package/dist/bin.mjs +574 -0
  3. package/package.json +95 -0
  4. package/src/bin/gen-api-client.test.ts +70 -0
  5. package/src/bin/gen-api-client.ts +986 -0
  6. package/src/client/headers.ts +31 -0
  7. package/src/client/index.ts +8 -0
  8. package/src/client/promise.ts +11 -0
  9. package/src/client/react/index.test.tsx +266 -0
  10. package/src/client/react/index.ts +431 -0
  11. package/src/client/responses.test.ts +151 -0
  12. package/src/client/responses.ts +278 -0
  13. package/src/client/transport.ts +74 -0
  14. package/src/client/transports/body-codec.ts +61 -0
  15. package/src/client/transports/fetch.ts +113 -0
  16. package/src/client/tsconfig.json +9 -0
  17. package/src/client/types.ts +15 -0
  18. package/src/client/url.ts +31 -0
  19. package/src/index.ts +63 -0
  20. package/src/router/fetch-types.ts +13 -0
  21. package/src/router/handlers/index.ts +2 -0
  22. package/src/router/handlers/openapi/index.ts +2 -0
  23. package/src/router/handlers/openapi/openapi.ts +293 -0
  24. package/src/router/integration/zod-openapi.test.ts +74 -0
  25. package/src/router/lib/charset.test.ts +22 -0
  26. package/src/router/lib/charset.ts +133 -0
  27. package/src/router/lib/collections.ts +3 -0
  28. package/src/router/lib/format.test.ts +67 -0
  29. package/src/router/lib/format.ts +35 -0
  30. package/src/router/lib/host.ts +4 -0
  31. package/src/router/lib/json-schema.ts +6 -0
  32. package/src/router/lib/media-type.test.ts +122 -0
  33. package/src/router/lib/media-type.ts +289 -0
  34. package/src/router/lib/pathname.test.ts +18 -0
  35. package/src/router/lib/pathname.ts +19 -0
  36. package/src/router/lib/route-names.ts +70 -0
  37. package/src/router/lib/route-normalize.test.ts +36 -0
  38. package/src/router/lib/route-normalize.ts +67 -0
  39. package/src/router/lib/schema-merge.ts +56 -0
  40. package/src/router/middleware/accept-ctx.test.ts +33 -0
  41. package/src/router/middleware/accept-ctx.ts +12 -0
  42. package/src/router/middleware/body-limit.test.ts +112 -0
  43. package/src/router/middleware/body-limit.ts +121 -0
  44. package/src/router/middleware/content-type-context.ts +0 -0
  45. package/src/router/middleware/cors.test.ts +269 -0
  46. package/src/router/middleware/cors.ts +490 -0
  47. package/src/router/middleware/csrf.test.ts +106 -0
  48. package/src/router/middleware/csrf.ts +192 -0
  49. package/src/router/middleware/define.ts +249 -0
  50. package/src/router/middleware/index.ts +34 -0
  51. package/src/router/middleware/jsxhtml-response.ts +0 -0
  52. package/src/router/middleware/oas-swagger.ts +0 -0
  53. package/src/router/middleware/rate-limit.test.ts +886 -0
  54. package/src/router/middleware/rate-limit.ts +920 -0
  55. package/src/router/middleware/request-id-ctx.test.ts +183 -0
  56. package/src/router/middleware/request-id-ctx.ts +135 -0
  57. package/src/router/middleware/request-logger-format.test.ts +16 -0
  58. package/src/router/middleware/request-logger-format.ts +269 -0
  59. package/src/router/middleware/request-logger.test.ts +267 -0
  60. package/src/router/middleware/request-logger.ts +131 -0
  61. package/src/router/middleware/start-time-ctx.ts +5 -0
  62. package/src/router/request.ts +611 -0
  63. package/src/router/response/core.ts +181 -0
  64. package/src/router/response/directives.ts +233 -0
  65. package/src/router/response/formats/content/bodyless.ts +54 -0
  66. package/src/router/response/formats/content/content.ts +79 -0
  67. package/src/router/response/formats/content/index.ts +2 -0
  68. package/src/router/response/formats/json-rpc/index.ts +2 -0
  69. package/src/router/response/formats/problem/badRequest.ts +90 -0
  70. package/src/router/response/formats/problem/conflict.ts +90 -0
  71. package/src/router/response/formats/problem/created.ts +40 -0
  72. package/src/router/response/formats/problem/index.ts +27 -0
  73. package/src/router/response/formats/problem/notFound.ts +90 -0
  74. package/src/router/response/formats/problem/permissionDenied.ts +90 -0
  75. package/src/router/response/formats/problem/problem.test.ts +888 -0
  76. package/src/router/response/formats/problem/rateLimited.ts +90 -0
  77. package/src/router/response/formats/problem/responses.ts +219 -0
  78. package/src/router/response/formats/problem/root-errors.ts +48 -0
  79. package/src/router/response/formats/problem/sessionExpired.ts +90 -0
  80. package/src/router/response/formats/problem/types.ts +170 -0
  81. package/src/router/response/formats/problem/unauthenticated.ts +90 -0
  82. package/src/router/response/formats/problem/valibot.ts +410 -0
  83. package/src/router/response/formats/status/index.ts +1 -0
  84. package/src/router/response/formats/status/responses.ts +59 -0
  85. package/src/router/response/formats/status/status.test.ts +21 -0
  86. package/src/router/response/framers.ts +85 -0
  87. package/src/router/response/index.ts +28 -0
  88. package/src/router/response/openapi.test.ts +96 -0
  89. package/src/router/response/openapi.ts +1 -0
  90. package/src/router/response/serializers.ts +66 -0
  91. package/src/router/response/stream.ts +35 -0
  92. package/src/router/router.test.ts +1571 -0
  93. package/src/router/router.ts +1965 -0
  94. package/src/router/routes/index.ts +46 -0
  95. package/src/router/routes/valibot/index.ts +18 -0
  96. package/src/router/routes/valibot/valibot.ts +1393 -0
  97. package/src/router/routes/valibot.test.ts +286 -0
  98. package/src/router/routes/zod/index.ts +18 -0
  99. package/src/router/routes/zod/zod.ts +1318 -0
  100. package/src/router/routes/zod.test.ts +280 -0
  101. package/src/router/server-interface.ts +31 -0
  102. 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`.