@mpen/routekit 0.1.5 → 0.1.6
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
CHANGED
|
@@ -429,6 +429,127 @@ Available Valibot APIs:
|
|
|
429
429
|
- `valibotSchemaMiddleware(options)`
|
|
430
430
|
- `defineValibotMiddleware(options)`
|
|
431
431
|
|
|
432
|
+
## Problem Responses
|
|
433
|
+
|
|
434
|
+
Routekit includes first-class support for standard problem responses inspired by RFC 7807 (Problem Details), with practical adjustments optimized for modern TypeScript and API clients (such as distinct `success: boolean` discriminators and clear nested `error` code/message structures). Using standard problem envelopes keeps error responses predictable, typed, and structured across your entire API.
|
|
435
|
+
|
|
436
|
+
### Response Helpers
|
|
437
|
+
|
|
438
|
+
Import standard response helpers from `@mpen/routekit/response/problem`:
|
|
439
|
+
|
|
440
|
+
```ts
|
|
441
|
+
import { HttpStatus } from '@mpen/http'
|
|
442
|
+
import {
|
|
443
|
+
ok,
|
|
444
|
+
created,
|
|
445
|
+
problem,
|
|
446
|
+
badRequest,
|
|
447
|
+
unauthenticated,
|
|
448
|
+
permissionDenied,
|
|
449
|
+
notFound,
|
|
450
|
+
conflict,
|
|
451
|
+
sessionExpired,
|
|
452
|
+
rateLimited,
|
|
453
|
+
} from '@mpen/routekit/response/problem'
|
|
454
|
+
|
|
455
|
+
// 200 OK standard success envelope
|
|
456
|
+
// Returns { success: true, data: { ... } }
|
|
457
|
+
router.get('/users/:id', () => ok({ id: 'user_123' }))
|
|
458
|
+
|
|
459
|
+
// 201 Created standard success envelope
|
|
460
|
+
// Returns { success: true, data: { ... } }
|
|
461
|
+
router.post('/users', () => created({ id: 'user_123' }))
|
|
462
|
+
|
|
463
|
+
// 404 Not Found problem details envelope
|
|
464
|
+
// Returns { success: false, error: { code: 'not_found', message: 'User not found' } }
|
|
465
|
+
router.get('/users/:id', ({ path }) => {
|
|
466
|
+
const user = findUser(path.id)
|
|
467
|
+
if (!user) {
|
|
468
|
+
return notFound('User not found')
|
|
469
|
+
}
|
|
470
|
+
return ok(user)
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
// Custom problem envelope
|
|
474
|
+
// Returns { success: false, error: { code: 'out_of_stock', message: 'Item is sold out', title: 'Sold Out' } }
|
|
475
|
+
router.post('/items/:id/buy', () => {
|
|
476
|
+
return problem({
|
|
477
|
+
status: HttpStatus.CONFLICT,
|
|
478
|
+
code: 'out_of_stock',
|
|
479
|
+
message: 'Item is sold out',
|
|
480
|
+
title: 'Sold Out',
|
|
481
|
+
})
|
|
482
|
+
})
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
All standard problem helpers (`badRequest`, `unauthenticated`, `notFound`, etc.) accept a human-readable `message` string as the first argument, or a detailed options object with custom headers and error code overrides.
|
|
486
|
+
|
|
487
|
+
### Router-level Errors
|
|
488
|
+
|
|
489
|
+
You can automatically map default router-level failures (such as `404 Not Found` for unmatched paths, `405 Method Not Allowed`, `415 Unsupported Media Type`, and uncaught server errors) to standard problem envelopes by installing the `problemRootErrors()` extension:
|
|
490
|
+
|
|
491
|
+
```ts
|
|
492
|
+
import { Router } from '@mpen/routekit'
|
|
493
|
+
import { problemRootErrors } from '@mpen/routekit/response/problem'
|
|
494
|
+
|
|
495
|
+
const router = new Router().install(problemRootErrors())
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
### Valibot Integration
|
|
499
|
+
|
|
500
|
+
Valibot helpers under `@mpen/routekit/response/problem/valibot` let you define schema boundaries and auto-validate endpoints using standard problem formats.
|
|
501
|
+
|
|
502
|
+
```ts
|
|
503
|
+
import { HttpStatus } from '@mpen/http'
|
|
504
|
+
import { ok } from '@mpen/routekit/response/problem'
|
|
505
|
+
import {
|
|
506
|
+
createValibotRouteBuilder,
|
|
507
|
+
okSchema,
|
|
508
|
+
problemSchema,
|
|
509
|
+
} from '@mpen/routekit/response/problem/valibot'
|
|
510
|
+
import * as v from 'valibot'
|
|
511
|
+
|
|
512
|
+
// Create a route builder pre-configured to handle request validation errors.
|
|
513
|
+
// It automatically responds with validation-failed problem envelopes on 400 or 422 errors.
|
|
514
|
+
const route = createValibotRouteBuilder()
|
|
515
|
+
|
|
516
|
+
router.post(
|
|
517
|
+
'/books',
|
|
518
|
+
route({
|
|
519
|
+
name: 'books.create',
|
|
520
|
+
schema: {
|
|
521
|
+
request: {
|
|
522
|
+
body: v.object({
|
|
523
|
+
title: v.string(),
|
|
524
|
+
author: v.string(),
|
|
525
|
+
}),
|
|
526
|
+
},
|
|
527
|
+
response: {
|
|
528
|
+
body: {
|
|
529
|
+
[HttpStatus.OK]: okSchema(
|
|
530
|
+
v.object({
|
|
531
|
+
id: v.string(),
|
|
532
|
+
title: v.string(),
|
|
533
|
+
}),
|
|
534
|
+
),
|
|
535
|
+
[HttpStatus.UNPROCESSABLE_ENTITY]: problemSchema({
|
|
536
|
+
code: v.literal('todo_limit_exceeded'),
|
|
537
|
+
}),
|
|
538
|
+
},
|
|
539
|
+
},
|
|
540
|
+
},
|
|
541
|
+
handler: ({ body }) => {
|
|
542
|
+
return ok({ id: '123', title: body.title })
|
|
543
|
+
},
|
|
544
|
+
}),
|
|
545
|
+
)
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
By default, the route builder created by `createValibotRouteBuilder()` uses `problemValidationErrorHandler` to format query, path, and body validation failures:
|
|
549
|
+
|
|
550
|
+
- Path and query parameters validation failures return `400 Bad Request` with `validation_failed:path` or `validation_failed:query` code.
|
|
551
|
+
- Request body validation failures return `422 Unprocessable Content` with `validation_failed:body` code and a list of structured `issues` indicating precisely where the failure occurred.
|
|
552
|
+
|
|
432
553
|
## OpenAPI
|
|
433
554
|
|
|
434
555
|
The `openapi()` handler reflects the active router's registered routes and schema metadata.
|
|
@@ -453,21 +574,53 @@ tags, security, and custom responses can be supplied beside the handler.
|
|
|
453
574
|
|
|
454
575
|
## Generated API Clients
|
|
455
576
|
|
|
456
|
-
|
|
457
|
-
typed client from each route's name, method, path, and JSON Schema metadata.
|
|
577
|
+
Expose typed endpoints to your frontend or consumer clients by generating a fully typed API client. The Routekit CLI loads your server's router module, reads `router.getRoutes()`, and outputs a strongly typed client mapping each route's name, method, path, and JSON Schema metadata.
|
|
458
578
|
|
|
459
579
|
```bash
|
|
460
|
-
|
|
580
|
+
$ bunx @mpen/routekit --help
|
|
581
|
+
Usage: bun run packages/routekit/src/bin/gen-api-client.ts <router-file> [options]
|
|
582
|
+
|
|
583
|
+
Generate a typed API client from a routekit router module.
|
|
584
|
+
|
|
585
|
+
Arguments:
|
|
586
|
+
router-file Router module that exports a router with getRoutes()
|
|
587
|
+
|
|
588
|
+
Options:
|
|
589
|
+
-o, --output <file> File to write. Prints to stdout when omitted.
|
|
590
|
+
-w, --write Write to <router-file>.gen.ts beside the router file.
|
|
591
|
+
-p, --pretty Format written output with Prettier.
|
|
592
|
+
-f, --format <format> Output format: rk-api-client or ts-query-rk-problem. Defaults to rk-api-client.
|
|
593
|
+
--client-name <Name> Generated client class name. Defaults to ApiClient.
|
|
594
|
+
--import-type <Type:module> Import a type used by generated schemas. Can be repeated.
|
|
595
|
+
--response-type <Type> Generic response wrapper type. Defaults to ApiResponsePromise.
|
|
596
|
+
--help Show this help message.
|
|
461
597
|
```
|
|
462
598
|
|
|
463
|
-
|
|
464
|
-
|
|
599
|
+
### Options Overview
|
|
600
|
+
|
|
601
|
+
- `<router-file>`: Path to the router module file. The module must export a Routekit `Router` instance as `default`, `router`, or another named export with a `getRoutes()` method.
|
|
602
|
+
- `-o, --output <file>`: File path to write. Prints to stdout when omitted.
|
|
603
|
+
- `-w, --write`: A convenient shortcut to write the generated code directly to `<router-file>.gen.ts` beside the router module.
|
|
604
|
+
- `-p, --pretty`: Automatically format the output using Prettier (requires Prettier to be installed).
|
|
605
|
+
- `-f, --format <format>`: Choice of output generator format:
|
|
606
|
+
- `rk-api-client` (default): Generates a nested, class-based HTTP client mapping properties to URL paths.
|
|
607
|
+
- `ts-query-rk-problem`: Generates TanStack Query integration options (`queryOptions`, `mutationOptions`) tailored for APIs using the standard `@mpen/routekit/response/problem` envelopes.
|
|
465
608
|
|
|
466
|
-
|
|
609
|
+
---
|
|
610
|
+
|
|
611
|
+
### Format: `rk-api-client` (Default)
|
|
612
|
+
|
|
613
|
+
This format generates a nested, typed class client:
|
|
614
|
+
|
|
615
|
+
```bash
|
|
616
|
+
bunx @mpen/routekit ./src/server/router.ts -w -p
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
Usage:
|
|
467
620
|
|
|
468
621
|
```ts
|
|
469
622
|
import { FetchTransport } from '@mpen/routekit/client'
|
|
470
|
-
import { ApiClient } from './
|
|
623
|
+
import { ApiClient } from './router.gen'
|
|
471
624
|
|
|
472
625
|
const client = new ApiClient(
|
|
473
626
|
new FetchTransport({
|
|
@@ -476,10 +629,10 @@ const client = new ApiClient(
|
|
|
476
629
|
}),
|
|
477
630
|
)
|
|
478
631
|
|
|
632
|
+
// Fully typed path parameters, query params, request body, and response union
|
|
479
633
|
const response = await client.books.byId.post({
|
|
480
634
|
path: 123,
|
|
481
635
|
body: { title: 'Dune', author: 'Frank Herbert' },
|
|
482
|
-
headers: { 'content-type': 'application/json' },
|
|
483
636
|
})
|
|
484
637
|
|
|
485
638
|
if (response.ok) {
|
|
@@ -488,8 +641,7 @@ if (response.ok) {
|
|
|
488
641
|
}
|
|
489
642
|
```
|
|
490
643
|
|
|
491
|
-
Routes with multiple documented response statuses generate a response union narrowed by
|
|
492
|
-
`response.status`:
|
|
644
|
+
Routes with multiple documented response statuses generate a response union narrowed by `response.status`:
|
|
493
645
|
|
|
494
646
|
```ts
|
|
495
647
|
const response = await client.widgets.byId.post(options)
|
|
@@ -500,9 +652,75 @@ if (response.status === 400) {
|
|
|
500
652
|
}
|
|
501
653
|
```
|
|
502
654
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
655
|
+
---
|
|
656
|
+
|
|
657
|
+
### Format: `ts-query-rk-problem` (TanStack Query + Problem Responses)
|
|
658
|
+
|
|
659
|
+
This format generates TanStack Query integration helpers tailored for standard `@mpen/routekit/response/problem` envelopes. Specify `-f ts-query-rk-problem` when running client generation:
|
|
660
|
+
|
|
661
|
+
```bash
|
|
662
|
+
bunx @mpen/routekit ./src/server/router.ts -f ts-query-rk-problem -w -p
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
The generated file exports `createApiQueryHelpers()`, which generates a nested helper structure containing `queryOptions` and `mutationOptions`:
|
|
666
|
+
|
|
667
|
+
```tsx
|
|
668
|
+
import { useQuery, useMutation } from '@tanstack/react-query'
|
|
669
|
+
import { FetchTransport } from '@mpen/routekit/client'
|
|
670
|
+
import { createApiQueryHelpers, isRoutekitProblemError } from './router.gen'
|
|
671
|
+
|
|
672
|
+
const transport = new FetchTransport({
|
|
673
|
+
baseUrl: 'https://api.example.com',
|
|
674
|
+
headers: () => ({ authorization: `Bearer ${token}` }),
|
|
675
|
+
})
|
|
676
|
+
|
|
677
|
+
const api = createApiQueryHelpers(transport)
|
|
678
|
+
|
|
679
|
+
// 1. Querying data with automatically unwrapped successful payloads
|
|
680
|
+
function BookDetails({ bookId }: { bookId: string }) {
|
|
681
|
+
const { data, error, isLoading } = useQuery(api.books.byId.get({ path: bookId }))
|
|
682
|
+
|
|
683
|
+
if (isLoading) return <div>Loading...</div>
|
|
684
|
+
|
|
685
|
+
// When a request returns a non-success problem envelope, error is typed as a RoutekitProblemError
|
|
686
|
+
if (error) {
|
|
687
|
+
if (isRoutekitProblemError(error)) {
|
|
688
|
+
return (
|
|
689
|
+
<div>
|
|
690
|
+
Error: {error.body.error.message} ({error.body.error.code})
|
|
691
|
+
</div>
|
|
692
|
+
)
|
|
693
|
+
}
|
|
694
|
+
return <div>Unknown error occurred</div>
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// Success data is automatically unwrapped from { success: true, data } envelope
|
|
698
|
+
return <h1>{data.title}</h1>
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// 2. Mutations with typed variables and problem error handling
|
|
702
|
+
function CreateBookForm() {
|
|
703
|
+
const mutation = useMutation(api.books.create.post())
|
|
704
|
+
|
|
705
|
+
const handleSubmit = (title: string, author: string) => {
|
|
706
|
+
mutation.mutate(
|
|
707
|
+
{
|
|
708
|
+
body: { title, author },
|
|
709
|
+
},
|
|
710
|
+
{
|
|
711
|
+
onError: (error) => {
|
|
712
|
+
if (isRoutekitProblemError(error) && error.status === 422) {
|
|
713
|
+
// Access structured validation issues
|
|
714
|
+
console.log(error.body.issues)
|
|
715
|
+
}
|
|
716
|
+
},
|
|
717
|
+
},
|
|
718
|
+
)
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// ...
|
|
722
|
+
}
|
|
723
|
+
```
|
|
506
724
|
|
|
507
725
|
## Exports
|
|
508
726
|
|
|
@@ -511,6 +729,8 @@ custom generic response wrapper.
|
|
|
511
729
|
- `@mpen/routekit/middleware` exports built-in middleware.
|
|
512
730
|
- `@mpen/routekit/handlers` exports `openapi()`.
|
|
513
731
|
- `@mpen/routekit/client` exports generated-client transports, response wrappers, body codecs, and URL/header helpers.
|
|
732
|
+
- `@mpen/routekit/response/problem` exports RFC 7807 problem details response helpers.
|
|
733
|
+
- `@mpen/routekit/response/problem/valibot` exports Valibot problem schemas and Valibot-specific problem route builders.
|
|
514
734
|
|
|
515
735
|
## Development
|
|
516
736
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { a as response } from "./core-DbmQauwS.mjs";
|
|
2
2
|
import { CommonContentTypes, CommonHeaders, HttpStatus } from "@mpen/http";
|
|
3
|
-
//#region src/router/lib/
|
|
3
|
+
//#region src/router/lib/fullwide.ts
|
|
4
4
|
const FULL_WIDE_FORMAT = new Intl.NumberFormat("en-US", {
|
|
5
5
|
useGrouping: false,
|
|
6
6
|
maximumFractionDigits: 20
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { A as stream, B as parseMediaType, C as isChunkDirective, D as isStatusDirective, E as isRoutekitDirective, F as mediaRangeToContentType, I as mediaTypeMatches, L as normalizeMediaType, N as mediaRangeAccepts, O as isStreamDirective, P as mediaRangeQuality, R as parseAcceptHeader, S as headers, T as isHeadersDirective, _ as jsonResponseBodySerializer, a as createRoutekitRequest, b as chunk, c as jsonRequestBodyParser, d as urlEncodedRequestBodyParser, f as defineMiddleware, g as defaultResponseBodySerializers, h as createStartStream, i as UnsupportedRequestBodyMediaTypeError, j as mergeRouteSchemas, k as status, l as responseFromRequestBodyError, m as createAsyncStream, n as RequestBodyLengthMismatchError, o as defaultRequestBodyParsers, p as isDeclaredMiddleware, r as RequestBodyTooLargeError, s as formDataRequestBodyParser, t as RequestBodyError, u as textRequestBodyParser, v as jsonLinesFramer, w as isHeadDirective, x as head, y as sseFramer, z as parseContentType } from "./request-Dn0zc-xm.mjs";
|
|
2
2
|
import { a as response, i as isRoutekitResponse, n as isResponseBodyInit, r as isRoutekitBody, t as body } from "./core-DbmQauwS.mjs";
|
|
3
|
-
import { a as text } from "./content-
|
|
3
|
+
import { a as text } from "./content-vVRhLG82.mjs";
|
|
4
4
|
import { CommonHeaders, HttpMethod, HttpStatus, StatusText } from "@mpen/http";
|
|
5
5
|
import { ConsoleLogger } from "@mpen/logger";
|
|
6
6
|
//#region src/router/lib/pathname.ts
|
package/dist/middleware.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { C as isChunkDirective, O as isStreamDirective, R as parseAcceptHeader, S as headers, T as isHeadersDirective, f as defineMiddleware, n as RequestBodyLengthMismatchError, r as RequestBodyTooLargeError, w as isHeadDirective } from "./request-Dn0zc-xm.mjs";
|
|
2
2
|
import { a as response, i as isRoutekitResponse, n as isResponseBodyInit } from "./core-DbmQauwS.mjs";
|
|
3
|
-
import { a as text, t as empty } from "./content-
|
|
3
|
+
import { a as text, t as empty } from "./content-vVRhLG82.mjs";
|
|
4
4
|
import { CommonHeaders, HttpMethod, HttpStatus } from "@mpen/http";
|
|
5
5
|
import { LogLevel } from "@mpen/logger";
|
|
6
6
|
//#region src/router/middleware/request-id-ctx.ts
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as text, i as html, n as noContent, r as redirect, t as empty } from "../content-
|
|
1
|
+
import { a as text, i as html, n as noContent, r as redirect, t as empty } from "../content-vVRhLG82.mjs";
|
|
2
2
|
export { empty, html, noContent, redirect, text };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mpen/routekit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Typed server-side routing utilities for Fetch-compatible runtimes.",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./dist/index.mjs",
|
|
@@ -81,9 +81,9 @@
|
|
|
81
81
|
"access": "public"
|
|
82
82
|
},
|
|
83
83
|
"publishHash": {
|
|
84
|
-
"buildDate": "2026-05-
|
|
85
|
-
"hgChangesetId": "
|
|
86
|
-
"sha256": "
|
|
84
|
+
"buildDate": "2026-05-31T01:27:13.624-07:00",
|
|
85
|
+
"hgChangesetId": "3e4d8a8fe08b+",
|
|
86
|
+
"sha256": "bee8f482030f29b2fd923f2ce390284a1aa6a1c3f598fb74fdc1465e06ea7058"
|
|
87
87
|
},
|
|
88
88
|
"inlinedDependencies": {
|
|
89
89
|
"@apidevtools/json-schema-ref-parser": "11.9.3",
|