@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
- `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.
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
- bun run routekit-gen-api-client ./src/server/router.ts -o ./src/client/api-client.gen.ts -p
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
- The router module must export a router instance as `default`, `router`, or another named
464
- export with a `getRoutes()` method.
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
- Generated clients use `@mpen/routekit/client`:
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 './api-client.gen'
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
- 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.
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/format.ts
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-BuDOmhH_.mjs";
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
@@ -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-BuDOmhH_.mjs";
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-BuDOmhH_.mjs";
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.5",
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-30T18:01:34.790-07:00",
85
- "hgChangesetId": "d5afc905fe8b+",
86
- "sha256": "e03b7e8d2bb4a3997752353a73e339f2d8f6ffecf40164dcbeefe10e98d89a3d"
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",