rouzer 2.0.0 → 2.0.1
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 +111 -0
- package/dist/client/index.d.ts +51 -23
- package/dist/client/index.js +10 -3
- package/dist/common.d.ts +7 -0
- package/dist/route.d.ts +35 -0
- package/dist/route.js +25 -0
- package/dist/server/router.d.ts +29 -15
- package/dist/server/router.js +8 -0
- package/dist/server/types.d.ts +8 -0
- package/dist/types.d.ts +75 -0
- package/docs/context.md +167 -0
- package/examples/basic-usage.ts +115 -0
- package/package.json +4 -1
- package/readme.md +0 -176
package/README.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Rouzer
|
|
2
|
+
|
|
3
|
+
Rouzer lets you declare a route once and share its TypeScript types and Zod
|
|
4
|
+
validation between a Hattip-compatible server and a typed fetch client.
|
|
5
|
+
|
|
6
|
+
## What it does
|
|
7
|
+
|
|
8
|
+
A Rouzer route declaration defines a URL pattern, method schemas, and optional
|
|
9
|
+
response type once, then reuses that contract to:
|
|
10
|
+
|
|
11
|
+
- validate client arguments before `fetch`
|
|
12
|
+
- match and validate server requests before handlers run
|
|
13
|
+
- type handler context from path, query/body, headers, and middleware
|
|
14
|
+
- attach typed client shorthand methods such as `client.helloRoute.GET(...)`
|
|
15
|
+
|
|
16
|
+
Rouzer optimizes for shared TypeScript route modules over language-agnostic API
|
|
17
|
+
schemas or generated SDKs.
|
|
18
|
+
|
|
19
|
+
## Is this for you?
|
|
20
|
+
|
|
21
|
+
Use Rouzer if:
|
|
22
|
+
|
|
23
|
+
- your server and client can import the same TypeScript route declarations
|
|
24
|
+
- you want Zod request validation on both sides of an HTTP boundary
|
|
25
|
+
- a Hattip-compatible handler fits your server runtime
|
|
26
|
+
- you prefer a small routing/client contract over a full web framework
|
|
27
|
+
|
|
28
|
+
Consider something else if:
|
|
29
|
+
|
|
30
|
+
- you need OpenAPI-first workflows, schema files, or generated clients for other
|
|
31
|
+
languages
|
|
32
|
+
- you need runtime response-body validation; `response: $type<T>()` is
|
|
33
|
+
compile-time only
|
|
34
|
+
- you want a framework that owns controllers, data loading, rendering, and
|
|
35
|
+
deployment adapters
|
|
36
|
+
- you cannot use ESM or Zod v4+
|
|
37
|
+
|
|
38
|
+
## Requirements
|
|
39
|
+
|
|
40
|
+
- ESM runtime and tooling
|
|
41
|
+
- Zod v4 or newer
|
|
42
|
+
- a Hattip adapter when using `createRouter(...)`
|
|
43
|
+
- a Fetch API implementation when using `createClient(...)`
|
|
44
|
+
- an absolute `baseURL` for pathname route patterns
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
pnpm add rouzer zod
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Import the public API from the root package:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { $type, chain, createClient, createRouter, route } from 'rouzer'
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`chain` is re-exported from `alien-middleware` for typed server middleware.
|
|
59
|
+
|
|
60
|
+
## Quick example
|
|
61
|
+
|
|
62
|
+
This example shows the core loop: one route contract defines validation, server
|
|
63
|
+
handler types, and the typed client call.
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import * as z from 'zod'
|
|
67
|
+
import { $type, createClient, createRouter, route } from 'rouzer'
|
|
68
|
+
|
|
69
|
+
export const helloRoute = route('hello/:name', {
|
|
70
|
+
GET: {
|
|
71
|
+
query: z.object({
|
|
72
|
+
excited: z.optional(z.boolean()),
|
|
73
|
+
}),
|
|
74
|
+
response: $type<{ message: string }>(),
|
|
75
|
+
},
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
export const routes = { helloRoute }
|
|
79
|
+
|
|
80
|
+
export const handler = createRouter({ basePath: 'api/' }).use(routes, {
|
|
81
|
+
helloRoute: {
|
|
82
|
+
GET(ctx) {
|
|
83
|
+
return {
|
|
84
|
+
message: `Hello, ${ctx.path.name}${ctx.query.excited ? '!' : '.'}`,
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const client = createClient({
|
|
91
|
+
baseURL: 'https://example.com/api/',
|
|
92
|
+
routes,
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
const { message } = await client.helloRoute.GET({
|
|
96
|
+
path: { name: 'world' },
|
|
97
|
+
query: { excited: true },
|
|
98
|
+
})
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`handler` can be mounted with any Hattip adapter. Client calls validate route
|
|
102
|
+
arguments before `fetch`; server handlers validate matched path, query, headers,
|
|
103
|
+
and JSON bodies before your handler runs.
|
|
104
|
+
|
|
105
|
+
## Documentation
|
|
106
|
+
|
|
107
|
+
- [Concepts and API selection](docs/context.md)
|
|
108
|
+
- [Runnable shared-route example](examples/basic-usage.ts)
|
|
109
|
+
- Generated declarations in the published package provide the exact signatures
|
|
110
|
+
for every public export.
|
|
111
|
+
- Public TSDoc in `src/` owns symbol-level behavior and option details.
|
package/dist/client/index.d.ts
CHANGED
|
@@ -1,61 +1,87 @@
|
|
|
1
1
|
import { Promisable } from '../common.js';
|
|
2
2
|
import { Route } from '../route.js';
|
|
3
3
|
import type { InferRouteResponse, RouteArgs, RouteRequest, RouteSchema } from '../types.js';
|
|
4
|
+
/** Client type inferred from a route map passed to `createClient`. */
|
|
4
5
|
export type RouzerClient<TRoutes extends Record<string, Route> = Record<string, never>> = ReturnType<typeof createClient<TRoutes>>;
|
|
6
|
+
/**
|
|
7
|
+
* Create a typed fetch client for Rouzer route declarations.
|
|
8
|
+
*
|
|
9
|
+
* @remarks The returned client always includes `request(...)` for raw responses
|
|
10
|
+
* and `json(...)` for parsed JSON. Passing `routes` also attaches shorthand
|
|
11
|
+
* methods such as `client.helloRoute.GET(...)`.
|
|
12
|
+
*/
|
|
5
13
|
export declare function createClient<TRoutes extends Record<string, Route> = Record<string, never>>(config: {
|
|
6
14
|
/**
|
|
7
|
-
*
|
|
15
|
+
* Absolute base URL used for pathname route patterns.
|
|
16
|
+
*
|
|
17
|
+
* @remarks A trailing slash is added when missing. In browsers, derive a
|
|
18
|
+
* relative API path with `new URL('/api/', window.location.origin).href`.
|
|
8
19
|
*/
|
|
9
20
|
baseURL: string;
|
|
10
21
|
/**
|
|
11
|
-
* Default headers
|
|
22
|
+
* Default headers sent with every request.
|
|
23
|
+
*
|
|
24
|
+
* @remarks Per-request headers are merged on top of these values. Undefined
|
|
25
|
+
* per-request headers are removed before `fetch`.
|
|
12
26
|
*/
|
|
13
27
|
headers?: Record<string, string>;
|
|
14
28
|
/**
|
|
15
|
-
*
|
|
29
|
+
* Route map to attach as shorthand methods on the client.
|
|
30
|
+
*
|
|
16
31
|
* @example
|
|
17
32
|
* ```ts
|
|
18
|
-
* const client = createClient({ baseURL: '/api/', routes
|
|
19
|
-
* client.helloRoute.GET({ path: { name: 'world' } })
|
|
33
|
+
* const client = createClient({ baseURL: 'https://example.com/api/', routes })
|
|
34
|
+
* await client.helloRoute.GET({ path: { name: 'world' } })
|
|
20
35
|
* ```
|
|
21
36
|
*/
|
|
22
37
|
routes?: TRoutes;
|
|
23
38
|
/**
|
|
24
|
-
* Custom handler for non-
|
|
25
|
-
*
|
|
39
|
+
* Custom handler for non-2xx responses from `.json()`.
|
|
40
|
+
*
|
|
41
|
+
* @remarks When provided, the return value is returned from `.json()` as-is;
|
|
42
|
+
* Rouzer does not automatically parse a `Response` returned by this hook.
|
|
43
|
+
* Without this hook, `.json()` throws an `Error` and copies JSON error-body
|
|
44
|
+
* properties onto it when the response has a JSON content type.
|
|
26
45
|
*/
|
|
27
46
|
onJsonError?: (response: Response) => Promisable<Response>;
|
|
28
|
-
/**
|
|
29
|
-
* Custom fetch implementation to use for requests.
|
|
30
|
-
*/
|
|
47
|
+
/** Custom `fetch` implementation to use for requests. */
|
|
31
48
|
fetch?: typeof globalThis.fetch;
|
|
32
49
|
}): { [K in keyof TRoutes]: TRoutes[K]["methods"] extends infer TMethods ? { [M in keyof TMethods]: RouteFunction<Extract<TMethods[M], RouteSchema>, TRoutes[K]["path"]["source"]>; } : never; } & {
|
|
33
50
|
config: {
|
|
34
51
|
/**
|
|
35
|
-
*
|
|
52
|
+
* Absolute base URL used for pathname route patterns.
|
|
53
|
+
*
|
|
54
|
+
* @remarks A trailing slash is added when missing. In browsers, derive a
|
|
55
|
+
* relative API path with `new URL('/api/', window.location.origin).href`.
|
|
36
56
|
*/
|
|
37
57
|
baseURL: string;
|
|
38
58
|
/**
|
|
39
|
-
* Default headers
|
|
59
|
+
* Default headers sent with every request.
|
|
60
|
+
*
|
|
61
|
+
* @remarks Per-request headers are merged on top of these values. Undefined
|
|
62
|
+
* per-request headers are removed before `fetch`.
|
|
40
63
|
*/
|
|
41
64
|
headers?: Record<string, string> | undefined;
|
|
42
65
|
/**
|
|
43
|
-
*
|
|
66
|
+
* Route map to attach as shorthand methods on the client.
|
|
67
|
+
*
|
|
44
68
|
* @example
|
|
45
69
|
* ```ts
|
|
46
|
-
* const client = createClient({ baseURL: '/api/', routes
|
|
47
|
-
* client.helloRoute.GET({ path: { name: 'world' } })
|
|
70
|
+
* const client = createClient({ baseURL: 'https://example.com/api/', routes })
|
|
71
|
+
* await client.helloRoute.GET({ path: { name: 'world' } })
|
|
48
72
|
* ```
|
|
49
73
|
*/
|
|
50
74
|
routes?: TRoutes | undefined;
|
|
51
75
|
/**
|
|
52
|
-
* Custom handler for non-
|
|
53
|
-
*
|
|
76
|
+
* Custom handler for non-2xx responses from `.json()`.
|
|
77
|
+
*
|
|
78
|
+
* @remarks When provided, the return value is returned from `.json()` as-is;
|
|
79
|
+
* Rouzer does not automatically parse a `Response` returned by this hook.
|
|
80
|
+
* Without this hook, `.json()` throws an `Error` and copies JSON error-body
|
|
81
|
+
* properties onto it when the response has a JSON content type.
|
|
54
82
|
*/
|
|
55
83
|
onJsonError?: ((response: Response) => Promisable<Response>) | undefined;
|
|
56
|
-
/**
|
|
57
|
-
* Custom fetch implementation to use for requests.
|
|
58
|
-
*/
|
|
84
|
+
/** Custom `fetch` implementation to use for requests. */
|
|
59
85
|
fetch?: typeof globalThis.fetch | undefined;
|
|
60
86
|
};
|
|
61
87
|
request: <T extends RouteRequest>({ path: pathBuilder, method, args: { path, query, body, headers }, schema, }: T) => Promise<Response & {
|
|
@@ -64,9 +90,11 @@ export declare function createClient<TRoutes extends Record<string, Route> = Rec
|
|
|
64
90
|
json: <T extends RouteRequest>(props: T) => Promise<T["$result"]>;
|
|
65
91
|
};
|
|
66
92
|
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
93
|
+
* Shorthand client method attached for each route method when `routes` is passed
|
|
94
|
+
* to `createClient`.
|
|
95
|
+
*
|
|
96
|
+
* @remarks Methods whose schema has `response: $type<T>()` return parsed JSON as
|
|
97
|
+
* `T`. Methods without a response marker return the raw `Response`.
|
|
70
98
|
*/
|
|
71
99
|
export type RouteFunction<T extends RouteSchema, P extends string> = (...p: RouteArgs<T, P> extends infer TArgs ? {} extends TArgs ? [args?: TArgs] : [args: TArgs] : never) => Promise<T extends {
|
|
72
100
|
response: any;
|
package/dist/client/index.js
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { mapValues, shake } from '../common.js';
|
|
2
|
+
/**
|
|
3
|
+
* Create a typed fetch client for Rouzer route declarations.
|
|
4
|
+
*
|
|
5
|
+
* @remarks The returned client always includes `request(...)` for raw responses
|
|
6
|
+
* and `json(...)` for parsed JSON. Passing `routes` also attaches shorthand
|
|
7
|
+
* methods such as `client.helloRoute.GET(...)`.
|
|
8
|
+
*/
|
|
2
9
|
export function createClient(config) {
|
|
3
10
|
const baseURL = config.baseURL.replace(/\/?$/, '/');
|
|
4
11
|
const defaultHeaders = config.headers && shake(config.headers);
|
|
@@ -31,9 +38,9 @@ export function createClient(config) {
|
|
|
31
38
|
}
|
|
32
39
|
if (headers) {
|
|
33
40
|
headers = shake(headers);
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
41
|
+
}
|
|
42
|
+
if (defaultHeaders) {
|
|
43
|
+
headers = headers ? { ...defaultHeaders, ...headers } : defaultHeaders;
|
|
37
44
|
}
|
|
38
45
|
if (schema.headers) {
|
|
39
46
|
headers = schema.headers.parse(headers);
|
package/dist/common.d.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
export type Promisable<T> = T | Promise<T>;
|
|
2
|
+
/**
|
|
3
|
+
* Compile-time-only marker used by `$type<T>()` to carry an unchecked response
|
|
4
|
+
* type through route declarations.
|
|
5
|
+
*
|
|
6
|
+
* @remarks Consumers usually use `$type<T>()` instead of constructing this type
|
|
7
|
+
* directly.
|
|
8
|
+
*/
|
|
2
9
|
export type Unchecked<T> = {
|
|
3
10
|
__unchecked__: T;
|
|
4
11
|
};
|
package/dist/route.d.ts
CHANGED
|
@@ -1,14 +1,49 @@
|
|
|
1
1
|
import { RoutePattern } from '@remix-run/route-pattern';
|
|
2
2
|
import { Unchecked } from './common.js';
|
|
3
3
|
import type { RouteRequestFactory, RouteSchema, RouteSchemaMap } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Create a compile-time-only marker for a route's JSON response payload type.
|
|
6
|
+
*
|
|
7
|
+
* @remarks `$type<T>()` does not perform runtime validation. It lets Rouzer type
|
|
8
|
+
* server handler return values and client shorthand methods for routes whose
|
|
9
|
+
* responses are expected to be JSON.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* const helloRoute = route('hello/:name', {
|
|
14
|
+
* GET: {
|
|
15
|
+
* response: $type<{ message: string }>(),
|
|
16
|
+
* },
|
|
17
|
+
* })
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
4
20
|
export declare function $type<T>(): Unchecked<T>;
|
|
5
21
|
export declare namespace $type {
|
|
6
22
|
var symbol: symbol;
|
|
7
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Shared route declaration produced by `route(...)`.
|
|
26
|
+
*
|
|
27
|
+
* @remarks A `Route` stores the parsed URL pattern, the method schema map, and a
|
|
28
|
+
* request factory for each declared method. Pass route maps to both
|
|
29
|
+
* `createRouter().use(...)` and `createClient({ routes })` to share the same
|
|
30
|
+
* contract on both sides of an HTTP boundary.
|
|
31
|
+
*/
|
|
8
32
|
export type Route<P extends string = string, T extends RouteSchemaMap = RouteSchemaMap> = {
|
|
33
|
+
/** Parsed route pattern used for URL generation and server-side matching. */
|
|
9
34
|
path: RoutePattern<P>;
|
|
35
|
+
/** Method schemas declared for this route. */
|
|
10
36
|
methods: T;
|
|
11
37
|
} & {
|
|
12
38
|
[K in keyof T]: RouteRequestFactory<Extract<T[K], RouteSchema>, P>;
|
|
13
39
|
};
|
|
40
|
+
/**
|
|
41
|
+
* Declare one URL pattern and its supported HTTP method schemas.
|
|
42
|
+
*
|
|
43
|
+
* @param pattern Route pattern parsed by `@remix-run/route-pattern`.
|
|
44
|
+
* @param methods Method schemas that describe request validation and optional
|
|
45
|
+
* response typing.
|
|
46
|
+
* @returns A shared route declaration with request factories such as `.GET(...)`
|
|
47
|
+
* and `.POST(...)` for the declared methods.
|
|
48
|
+
*/
|
|
14
49
|
export declare function route<P extends string, T extends RouteSchemaMap>(pattern: P, methods: T): Route<P, T>;
|
package/dist/route.js
CHANGED
|
@@ -1,9 +1,34 @@
|
|
|
1
1
|
import { RoutePattern } from '@remix-run/route-pattern';
|
|
2
2
|
import { mapEntries } from './common.js';
|
|
3
|
+
/**
|
|
4
|
+
* Create a compile-time-only marker for a route's JSON response payload type.
|
|
5
|
+
*
|
|
6
|
+
* @remarks `$type<T>()` does not perform runtime validation. It lets Rouzer type
|
|
7
|
+
* server handler return values and client shorthand methods for routes whose
|
|
8
|
+
* responses are expected to be JSON.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* const helloRoute = route('hello/:name', {
|
|
13
|
+
* GET: {
|
|
14
|
+
* response: $type<{ message: string }>(),
|
|
15
|
+
* },
|
|
16
|
+
* })
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
3
19
|
export function $type() {
|
|
4
20
|
return $type.symbol;
|
|
5
21
|
}
|
|
6
22
|
$type.symbol = Symbol();
|
|
23
|
+
/**
|
|
24
|
+
* Declare one URL pattern and its supported HTTP method schemas.
|
|
25
|
+
*
|
|
26
|
+
* @param pattern Route pattern parsed by `@remix-run/route-pattern`.
|
|
27
|
+
* @param methods Method schemas that describe request validation and optional
|
|
28
|
+
* response typing.
|
|
29
|
+
* @returns A shared route declaration with request factories such as `.GET(...)`
|
|
30
|
+
* and `.POST(...)` for the declared methods.
|
|
31
|
+
*/
|
|
7
32
|
export function route(pattern, methods) {
|
|
8
33
|
const path = new RoutePattern(pattern);
|
|
9
34
|
const createFetch = (method, schema) => (args = {}) => {
|
package/dist/server/router.d.ts
CHANGED
|
@@ -3,34 +3,36 @@ import { ApplyMiddleware, chain, ExtractMiddleware, MiddlewareChain, MiddlewareT
|
|
|
3
3
|
import type { Routes } from '../types.js';
|
|
4
4
|
import type { RouteRequestHandlerMap } from './types.js';
|
|
5
5
|
export { chain };
|
|
6
|
+
/** Configuration for `createRouter`. */
|
|
6
7
|
export type RouterConfig = {
|
|
7
8
|
/**
|
|
8
|
-
* Base path to prepend to all
|
|
9
|
+
* Base path to prepend to all route patterns.
|
|
10
|
+
*
|
|
11
|
+
* @remarks Leading and trailing slashes are normalized so `api`, `/api`, and
|
|
12
|
+
* `api/` all mount routes under `/api/`.
|
|
13
|
+
*
|
|
9
14
|
* @example
|
|
10
15
|
* ```ts
|
|
11
|
-
* basePath: 'api/'
|
|
16
|
+
* createRouter({ basePath: 'api/' })
|
|
12
17
|
* ```
|
|
13
18
|
*/
|
|
14
19
|
basePath?: string;
|
|
15
20
|
/**
|
|
16
|
-
* Enable
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* debug: process.env.NODE_ENV !== 'production',
|
|
22
|
-
* ```
|
|
21
|
+
* Enable debug behavior for local development.
|
|
22
|
+
*
|
|
23
|
+
* @remarks Debug mode adds an `X-Route-Name` response header for matched
|
|
24
|
+
* routes, includes specific Zod error messages in `400` validation responses,
|
|
25
|
+
* and logs missing route handlers to the console.
|
|
23
26
|
*/
|
|
24
27
|
debug?: boolean;
|
|
25
|
-
/**
|
|
26
|
-
* CORS configuration.
|
|
27
|
-
*/
|
|
28
|
+
/** CORS configuration for requests with an `Origin` header. */
|
|
28
29
|
cors?: {
|
|
29
30
|
/**
|
|
30
|
-
*
|
|
31
|
+
* Allowed origins for CORS requests.
|
|
31
32
|
*
|
|
32
|
-
* Origins may contain wildcards for protocol and subdomain. The
|
|
33
|
-
* optional and defaults to `https`.
|
|
33
|
+
* @remarks Origins may contain wildcards for protocol and subdomain. The
|
|
34
|
+
* protocol is optional and defaults to `https`. Requests with an `Origin`
|
|
35
|
+
* header outside this list receive `403`.
|
|
34
36
|
*
|
|
35
37
|
* @example
|
|
36
38
|
* ```ts
|
|
@@ -40,6 +42,10 @@ export type RouterConfig = {
|
|
|
40
42
|
allowOrigins?: string[];
|
|
41
43
|
};
|
|
42
44
|
};
|
|
45
|
+
/**
|
|
46
|
+
* Hattip-compatible Rouzer handler with chainable middleware and route
|
|
47
|
+
* registration.
|
|
48
|
+
*/
|
|
43
49
|
export interface Router<T extends MiddlewareTypes = any> extends HattipHandler<T['platform']>, MiddlewareChain<T> {
|
|
44
50
|
/**
|
|
45
51
|
* Clone this router and add the given middleware to the end of the chain.
|
|
@@ -54,4 +60,12 @@ export interface Router<T extends MiddlewareTypes = any> extends HattipHandler<T
|
|
|
54
60
|
*/
|
|
55
61
|
use<TRoutes extends Routes>(routes: TRoutes, handlers: RouteRequestHandlerMap<TRoutes, this>): Router<T>;
|
|
56
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Create a Rouzer router that can be mounted by any Hattip adapter.
|
|
65
|
+
*
|
|
66
|
+
* @param config Optional router configuration for base path, debug behavior, and
|
|
67
|
+
* CORS origin restrictions.
|
|
68
|
+
* @returns A Hattip-compatible handler with `.use(...)` methods for middleware
|
|
69
|
+
* and route registration.
|
|
70
|
+
*/
|
|
57
71
|
export declare function createRouter<TEnv extends object = {}, TProperties extends object = {}, TPlatform = unknown>(config?: RouterConfig): Router<MiddlewareTypes<TEnv, TProperties, TPlatform>>;
|
package/dist/server/router.js
CHANGED
|
@@ -139,6 +139,14 @@ class RouterObject extends MiddlewareChain {
|
|
|
139
139
|
});
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* Create a Rouzer router that can be mounted by any Hattip adapter.
|
|
144
|
+
*
|
|
145
|
+
* @param config Optional router configuration for base path, debug behavior, and
|
|
146
|
+
* CORS origin restrictions.
|
|
147
|
+
* @returns A Hattip-compatible handler with `.use(...)` methods for middleware
|
|
148
|
+
* and route registration.
|
|
149
|
+
*/
|
|
142
150
|
export function createRouter(config = {}) {
|
|
143
151
|
const router = new RouterObject(config);
|
|
144
152
|
const handler = router.toHandler();
|
package/dist/server/types.d.ts
CHANGED
|
@@ -26,6 +26,14 @@ type InferRouteRequestHandler<TMiddleware extends AnyMiddlewareChain, TSchema ex
|
|
|
26
26
|
headers: any;
|
|
27
27
|
} ? z.infer<TSchema['headers']> : undefined;
|
|
28
28
|
}, InferRouteResponse<TSchema>>;
|
|
29
|
+
/**
|
|
30
|
+
* Handler map shape required by `createRouter().use(routes, handlers)`.
|
|
31
|
+
*
|
|
32
|
+
* @remarks Each route key must provide handlers for the methods declared by its
|
|
33
|
+
* route schema. Handler context is inferred from middleware plus the route's
|
|
34
|
+
* path, query/body, and header schemas. An optional `OPTIONS` handler can
|
|
35
|
+
* customize CORS preflight responses for a route.
|
|
36
|
+
*/
|
|
29
37
|
export type RouteRequestHandlerMap<TRoutes extends Routes = Routes, TMiddleware extends AnyMiddlewareChain = MiddlewareChain> = {
|
|
30
38
|
[K in keyof TRoutes]: {
|
|
31
39
|
[TMethod in keyof TRoutes[K]['methods']]: InferRouteRequestHandler<TMiddleware, Extract<TRoutes[K]['methods'][TMethod], RouteSchema>, Extract<TMethod, string>, TRoutes[K]['path']['source']>;
|
package/dist/types.d.ts
CHANGED
|
@@ -1,21 +1,46 @@
|
|
|
1
1
|
import { Params, RoutePattern } from '@remix-run/route-pattern';
|
|
2
2
|
import * as z from 'zod';
|
|
3
3
|
import { Unchecked } from './common.js';
|
|
4
|
+
/**
|
|
5
|
+
* Compile-time-only marker used by `$type<T>()` for unchecked response types.
|
|
6
|
+
*
|
|
7
|
+
* @remarks Application code should usually call `$type<T>()` instead of naming
|
|
8
|
+
* this marker directly.
|
|
9
|
+
*/
|
|
4
10
|
export type { Unchecked };
|
|
11
|
+
/** Schema shape for `GET` route methods. */
|
|
5
12
|
export type QueryRouteSchema = {
|
|
13
|
+
/** Optional Zod object used to validate path params. */
|
|
6
14
|
path?: z.ZodObject<any>;
|
|
15
|
+
/** Optional Zod object used to validate URL query params. */
|
|
7
16
|
query?: z.ZodObject<any>;
|
|
17
|
+
/** `GET` routes do not accept request bodies. */
|
|
8
18
|
body?: never;
|
|
19
|
+
/** Optional Zod object used to validate request headers. */
|
|
9
20
|
headers?: z.ZodObject<any>;
|
|
21
|
+
/** Optional compile-time-only JSON response type marker. */
|
|
10
22
|
response?: Unchecked<any>;
|
|
11
23
|
};
|
|
24
|
+
/** Schema shape for mutation route methods. */
|
|
12
25
|
export type MutationRouteSchema = {
|
|
26
|
+
/** Optional Zod object used to validate path params. */
|
|
13
27
|
path?: z.ZodObject<any>;
|
|
28
|
+
/** Mutation routes do not accept query schemas. */
|
|
14
29
|
query?: never;
|
|
30
|
+
/** Optional Zod schema used to validate the JSON request body. */
|
|
15
31
|
body?: z.ZodType<any, any>;
|
|
32
|
+
/** Optional Zod object used to validate request headers. */
|
|
16
33
|
headers?: z.ZodObject<any>;
|
|
34
|
+
/** Optional compile-time-only JSON response type marker. */
|
|
17
35
|
response?: Unchecked<any>;
|
|
18
36
|
};
|
|
37
|
+
/**
|
|
38
|
+
* Method schema map accepted by `route(...)`.
|
|
39
|
+
*
|
|
40
|
+
* @remarks `GET` validates query input, mutation methods validate JSON body
|
|
41
|
+
* input, and `ALL` acts as a fallback for methods that are not declared
|
|
42
|
+
* explicitly.
|
|
43
|
+
*/
|
|
19
44
|
export type RouteSchemaMap = {
|
|
20
45
|
GET?: QueryRouteSchema;
|
|
21
46
|
POST?: MutationRouteSchema;
|
|
@@ -23,14 +48,21 @@ export type RouteSchemaMap = {
|
|
|
23
48
|
PATCH?: MutationRouteSchema;
|
|
24
49
|
DELETE?: MutationRouteSchema;
|
|
25
50
|
ALL?: {
|
|
51
|
+
/** Optional Zod object used to validate path params. */
|
|
26
52
|
path?: z.ZodObject<any>;
|
|
53
|
+
/** Optional Zod object used to validate URL query params. */
|
|
27
54
|
query?: z.ZodObject<any>;
|
|
55
|
+
/** `ALL` fallback routes do not accept request bodies. */
|
|
28
56
|
body?: never;
|
|
57
|
+
/** Optional Zod object used to validate request headers. */
|
|
29
58
|
headers?: z.ZodObject<any>;
|
|
59
|
+
/** `ALL` fallback routes do not define typed JSON responses. */
|
|
30
60
|
response?: never;
|
|
31
61
|
};
|
|
32
62
|
};
|
|
63
|
+
/** Any route method schema Rouzer can execute. */
|
|
33
64
|
export type RouteSchema = QueryRouteSchema | MutationRouteSchema;
|
|
65
|
+
/** Route map accepted by `createRouter().use(...)` and `createClient(...)`. */
|
|
34
66
|
export type Routes = {
|
|
35
67
|
[key: string]: {
|
|
36
68
|
path: RoutePattern;
|
|
@@ -67,23 +99,46 @@ type MutationArgs<T> = T extends MutationRouteSchema ? T extends {
|
|
|
67
99
|
} : {
|
|
68
100
|
body?: unknown;
|
|
69
101
|
} : unknown;
|
|
102
|
+
/**
|
|
103
|
+
* Arguments accepted by a route request factory such as `route.GET(...)`.
|
|
104
|
+
*
|
|
105
|
+
* @remarks The type is derived from a method schema and route pattern. `path`,
|
|
106
|
+
* `query`, `body`, and `headers` are validated by the client before `fetch` when
|
|
107
|
+
* a matching schema exists. The client forwards the HTTP method, JSON body, and
|
|
108
|
+
* headers; extra `RequestInit` fields are accepted by the type surface but are
|
|
109
|
+
* not forwarded.
|
|
110
|
+
*/
|
|
70
111
|
export type RouteArgs<T extends RouteSchema = any, P extends string = string> = ([T] extends [Any] ? {
|
|
71
112
|
query?: any;
|
|
72
113
|
body?: any;
|
|
73
114
|
path?: any;
|
|
74
115
|
} : QueryArgs<T> & MutationArgs<T> & PathArgs<T, P>) & Omit<RequestInit, 'method' | 'body' | 'headers'> & {
|
|
116
|
+
/** Headers for this request. Undefined values are removed before `fetch`. */
|
|
75
117
|
headers?: Record<string, string | undefined>;
|
|
76
118
|
};
|
|
119
|
+
/**
|
|
120
|
+
* Request descriptor produced by a route request factory.
|
|
121
|
+
*
|
|
122
|
+
* @remarks Pass this object to `client.request(...)` for a raw `Response` or
|
|
123
|
+
* `client.json(...)` for parsed JSON handling.
|
|
124
|
+
*/
|
|
77
125
|
export type RouteRequest<TResult = any> = {
|
|
126
|
+
/** Method schema used for client-side validation. */
|
|
78
127
|
schema: RouteSchema;
|
|
128
|
+
/** Parsed route pattern used to generate the request URL. */
|
|
79
129
|
path: RoutePattern;
|
|
130
|
+
/** HTTP method to send. */
|
|
80
131
|
method: string;
|
|
132
|
+
/** Validated route arguments and request options. */
|
|
81
133
|
args: RouteArgs;
|
|
134
|
+
/** Phantom result type consumed by `client.json(...)`. */
|
|
82
135
|
$result: TResult;
|
|
83
136
|
};
|
|
137
|
+
/** `Response` whose `.json()` method resolves to a known payload type. */
|
|
84
138
|
export type RouteResponse<TResult = any> = Response & {
|
|
85
139
|
json(): Promise<TResult>;
|
|
86
140
|
};
|
|
141
|
+
/** Infer the JSON response payload type from a method schema. */
|
|
87
142
|
export type InferRouteResponse<T extends RouteSchema> = T extends {
|
|
88
143
|
response: Unchecked<infer TResponse>;
|
|
89
144
|
} ? TResponse : void;
|
|
@@ -93,12 +148,32 @@ type InferRouteSchemaBody<TSchema> = TSchema extends MutationRouteSchema ? TSche
|
|
|
93
148
|
type InferRouteArgsBody<TArgs> = TArgs extends {
|
|
94
149
|
body?: infer TBody;
|
|
95
150
|
} ? TBody : never;
|
|
151
|
+
/**
|
|
152
|
+
* Infer the request body type from a mutation schema or route request factory.
|
|
153
|
+
*
|
|
154
|
+
* @remarks Route request factories for mutation methods infer their `body`
|
|
155
|
+
* argument type. Mutation schemas without a body schema infer `unknown`.
|
|
156
|
+
*/
|
|
96
157
|
export type InferRouteBody<T> = T extends RouteRequestFactory<any, any> ? InferRouteArgsBody<T['$args']> : T extends RouteSchema ? InferRouteSchemaBody<T> : never;
|
|
158
|
+
/**
|
|
159
|
+
* Infer the request body type for a named method on a `Route`.
|
|
160
|
+
*
|
|
161
|
+
* @remarks `GET` and `ALL` infer `never` because they do not accept request
|
|
162
|
+
* bodies.
|
|
163
|
+
*/
|
|
97
164
|
export type InferRouteMethodBody<TRoute extends {
|
|
98
165
|
methods: RouteSchemaMap;
|
|
99
166
|
}, TMethod extends keyof TRoute['methods']> = TMethod extends 'GET' | 'ALL' ? never : TMethod extends keyof TRoute ? InferRouteBody<TRoute[TMethod]> : InferRouteBody<Extract<TRoute['methods'][TMethod], RouteSchema>>;
|
|
167
|
+
/**
|
|
168
|
+
* Callable factory attached to a `Route` for each declared method.
|
|
169
|
+
*
|
|
170
|
+
* @remarks Calling a factory validates no data by itself; it creates a typed
|
|
171
|
+
* `RouteRequest` descriptor for `createClient` to validate and send.
|
|
172
|
+
*/
|
|
100
173
|
export type RouteRequestFactory<T extends RouteSchema, P extends string> = {
|
|
101
174
|
(...p: RouteArgs<T, P> extends infer TArgs ? {} extends TArgs ? [args?: TArgs] : [args: TArgs] : never): RouteRequest<InferRouteResponse<T>>;
|
|
175
|
+
/** Inferred argument type for this request factory. */
|
|
102
176
|
$args: RouteArgs<T, P>;
|
|
177
|
+
/** Inferred JSON response type for this request factory. */
|
|
103
178
|
$response: InferRouteResponse<T>;
|
|
104
179
|
};
|
package/docs/context.md
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Rouzer context
|
|
2
|
+
|
|
3
|
+
Rouzer is for applications that want one route contract to drive both the HTTP
|
|
4
|
+
server and the client that calls it. A route declaration combines a URL pattern,
|
|
5
|
+
HTTP method schemas, and an optional compile-time response type.
|
|
6
|
+
|
|
7
|
+
## When to use Rouzer
|
|
8
|
+
|
|
9
|
+
Use Rouzer when:
|
|
10
|
+
|
|
11
|
+
- the same TypeScript project, package, or workspace can share route
|
|
12
|
+
declarations between server and client code
|
|
13
|
+
- request validation should run before server handlers and before client `fetch`
|
|
14
|
+
calls
|
|
15
|
+
- a Hattip-compatible handler fits your server runtime
|
|
16
|
+
- generated clients should stay close to the route definitions instead of being
|
|
17
|
+
produced by a separate OpenAPI build step
|
|
18
|
+
|
|
19
|
+
Rouzer is not a response validation library, an OpenAPI generator, or a complete
|
|
20
|
+
server framework. It focuses on typed route contracts, validation, routing, and a
|
|
21
|
+
small client wrapper.
|
|
22
|
+
|
|
23
|
+
## Core abstractions
|
|
24
|
+
|
|
25
|
+
### Route declarations
|
|
26
|
+
|
|
27
|
+
Declare routes with `route(pattern, methods)`. The pattern is parsed by
|
|
28
|
+
`@remix-run/route-pattern`, so route params can be inferred from patterns such
|
|
29
|
+
as `hello/:name`, `v:major.:minor`, `api(/v:major(.:minor))`, `assets/*path`,
|
|
30
|
+
`search?q`, or full URL patterns such as
|
|
31
|
+
`https://:store.shopify.com/orders`.
|
|
32
|
+
|
|
33
|
+
Method schemas describe the request pieces Rouzer should validate:
|
|
34
|
+
|
|
35
|
+
| Method kind | Request schemas | Notes |
|
|
36
|
+
| -------------------------------- | -------------------------------------- | --------------------------------------------------------------------------------------- |
|
|
37
|
+
| `GET` | `path`, `query`, `headers`, `response` | No request body. |
|
|
38
|
+
| `POST`, `PUT`, `PATCH`, `DELETE` | `path`, `body`, `headers`, `response` | No query schema. |
|
|
39
|
+
| `ALL` | `path`, `query`, `headers` | Fallback when the incoming method is not explicitly declared. No body or response type. |
|
|
40
|
+
|
|
41
|
+
If you omit a `path` schema, TypeScript infers path params from the pattern and
|
|
42
|
+
server handlers receive them as strings. Add a Zod `path` schema when you need
|
|
43
|
+
runtime validation, transforms, or non-string handler types.
|
|
44
|
+
|
|
45
|
+
### `$type<T>()`
|
|
46
|
+
|
|
47
|
+
`response: $type<T>()` is a TypeScript-only marker. It tells handlers and client
|
|
48
|
+
shorthand methods what response payload type to expect, but Rouzer does not
|
|
49
|
+
validate response bodies at runtime.
|
|
50
|
+
|
|
51
|
+
Routes without a `response` marker return a raw `Response` from client shorthand
|
|
52
|
+
methods. Routes with a `response` marker use `client.json(...)` under the hood
|
|
53
|
+
and return parsed JSON typed as `T`.
|
|
54
|
+
|
|
55
|
+
### Router
|
|
56
|
+
|
|
57
|
+
`createRouter()` returns a Hattip-compatible handler. Use `.use(middleware)` to
|
|
58
|
+
append typed `alien-middleware` middleware and `.use(routes, handlers)` to attach
|
|
59
|
+
route handlers.
|
|
60
|
+
|
|
61
|
+
Handlers receive a context typed from middleware plus the route schema:
|
|
62
|
+
|
|
63
|
+
- `GET` handlers receive `ctx.path`, `ctx.query`, and `ctx.headers`
|
|
64
|
+
- mutation handlers receive `ctx.path`, `ctx.body`, and `ctx.headers`
|
|
65
|
+
- handlers may return a plain JSON-serializable value or a `Response`
|
|
66
|
+
- plain values are returned with `Response.json(value)`
|
|
67
|
+
- return a `Response` when you need custom status, headers, or body handling
|
|
68
|
+
|
|
69
|
+
`basePath` is prepended to route patterns, `debug` adds matched-route debug
|
|
70
|
+
headers and more detailed validation errors, and `cors.allowOrigins` restricts
|
|
71
|
+
requests with an `Origin` header.
|
|
72
|
+
|
|
73
|
+
### Client
|
|
74
|
+
|
|
75
|
+
`createClient({ baseURL, routes })` creates:
|
|
76
|
+
|
|
77
|
+
- `client.request(route.GET(args))` for a raw `Response`
|
|
78
|
+
- `client.json(route.GET(args))` for parsed JSON and default non-2xx throwing
|
|
79
|
+
- shorthand methods such as `client.helloRoute.GET(args)` when `routes` is
|
|
80
|
+
supplied
|
|
81
|
+
|
|
82
|
+
Prefer an absolute `baseURL` for pathname route patterns:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
const client = createClient({
|
|
86
|
+
baseURL: new URL('/api/', window.location.origin).href,
|
|
87
|
+
routes,
|
|
88
|
+
})
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Default headers can be supplied with `headers`, per-request headers are merged on
|
|
92
|
+
top, and a custom `fetch` implementation can be supplied for tests or non-browser
|
|
93
|
+
runtimes.
|
|
94
|
+
|
|
95
|
+
## Lifecycle
|
|
96
|
+
|
|
97
|
+
1. Define shared route declarations with `route(...)` and Zod schemas.
|
|
98
|
+
2. Attach those routes to a server with `createRouter().use(routes, handlers)`.
|
|
99
|
+
3. Create a client with the same route map.
|
|
100
|
+
4. Client calls validate `path`, `query`, `body`, and `headers` before `fetch`.
|
|
101
|
+
5. The router matches the request, validates the matched inputs, and calls the
|
|
102
|
+
handler.
|
|
103
|
+
6. Plain handler results become JSON responses; explicit `Response` objects pass
|
|
104
|
+
through unchanged.
|
|
105
|
+
|
|
106
|
+
On the server, `path`, `query`, and `headers` values originate as strings. Rouzer
|
|
107
|
+
coerces Zod `number` schemas with `Number(value)` and Zod `boolean` schemas from
|
|
108
|
+
`"true"` and `"false"`. JSON request bodies are parsed and validated without that
|
|
109
|
+
string-coercion step.
|
|
110
|
+
|
|
111
|
+
## Common tasks
|
|
112
|
+
|
|
113
|
+
### Choose a client call style
|
|
114
|
+
|
|
115
|
+
Use shorthand methods for normal application calls:
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
await client.helloRoute.GET({ path: { name: 'Ada' } })
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Use longhand calls when you need to choose response handling explicitly:
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
const response = await client.request(
|
|
125
|
+
routes.helloRoute.GET({ path: { name: 'Ada' } })
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
const json = await client.json(routes.helloRoute.GET({ path: { name: 'Ada' } }))
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Return custom responses
|
|
132
|
+
|
|
133
|
+
Return a `Response` from a handler for non-JSON payloads, custom status codes, or
|
|
134
|
+
custom headers. Return a plain value for the default `Response.json(value)` path.
|
|
135
|
+
|
|
136
|
+
### Customize JSON errors
|
|
137
|
+
|
|
138
|
+
By default, `client.json(...)` throws for non-2xx responses. If the response body
|
|
139
|
+
is JSON, its properties are copied onto the thrown `Error`.
|
|
140
|
+
|
|
141
|
+
`onJsonError` can override that behavior. Its return value is returned from
|
|
142
|
+
`client.json(...)` as-is; Rouzer does not automatically parse a returned
|
|
143
|
+
`Response` from `onJsonError`.
|
|
144
|
+
|
|
145
|
+
## Patterns to prefer
|
|
146
|
+
|
|
147
|
+
- Export route declarations from a small shared module and import that module on
|
|
148
|
+
both server and client.
|
|
149
|
+
- Add Zod schemas when you need runtime guarantees; rely on inferred path params
|
|
150
|
+
only when string params are sufficient.
|
|
151
|
+
- Use `response: $type<T>()` for JSON endpoints that should have typed client
|
|
152
|
+
shorthand methods.
|
|
153
|
+
- Use explicit HTTP methods when you want precise handler context types; reserve
|
|
154
|
+
`ALL` for true fallback behavior.
|
|
155
|
+
- Set `content-type: application/json` yourself when your server or middleware
|
|
156
|
+
depends on that header.
|
|
157
|
+
|
|
158
|
+
## Constraints and gotchas
|
|
159
|
+
|
|
160
|
+
- `$type<T>()` is compile-time only and does not validate response payloads.
|
|
161
|
+
- Pathname route patterns expect an absolute client `baseURL`.
|
|
162
|
+
- Extra `RequestInit` fields in route args, such as `signal` or `credentials`,
|
|
163
|
+
are accepted by the type surface but are not forwarded by `createClient`.
|
|
164
|
+
- `ALL` can declare `query`, but handler context typing is less precise than
|
|
165
|
+
explicit `GET` handlers.
|
|
166
|
+
- Rouzer does not automatically set `Access-Control-Allow-Credentials`; set it in
|
|
167
|
+
your handler when credentialed cross-origin requests need it.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { HattipHandler } from '@hattip/core'
|
|
2
|
+
import * as z from 'zod'
|
|
3
|
+
import { $type, chain, createClient, createRouter, route } from 'rouzer'
|
|
4
|
+
|
|
5
|
+
type Profile = {
|
|
6
|
+
id: string
|
|
7
|
+
name: string
|
|
8
|
+
includePosts: boolean
|
|
9
|
+
requestId: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const profileRoute = route('profiles/:id', {
|
|
13
|
+
GET: {
|
|
14
|
+
query: z.object({
|
|
15
|
+
includePosts: z.optional(z.boolean()),
|
|
16
|
+
}),
|
|
17
|
+
response: $type<Profile>(),
|
|
18
|
+
},
|
|
19
|
+
PATCH: {
|
|
20
|
+
body: z.object({
|
|
21
|
+
name: z.string().check(z.minLength(1)),
|
|
22
|
+
}),
|
|
23
|
+
headers: z.object({
|
|
24
|
+
'content-type': z.literal('application/json'),
|
|
25
|
+
}),
|
|
26
|
+
response: $type<Profile>(),
|
|
27
|
+
},
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
export const routes = { profileRoute }
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Tiny Hattip adapter used only to keep this example self-contained. Real apps
|
|
34
|
+
* mount the handler with a Hattip adapter for their runtime.
|
|
35
|
+
*/
|
|
36
|
+
function createLocalFetch(handler: HattipHandler): typeof fetch {
|
|
37
|
+
return async (input, init) => {
|
|
38
|
+
const request = new Request(input, init)
|
|
39
|
+
const response = await handler({
|
|
40
|
+
request,
|
|
41
|
+
ip: '127.0.0.1',
|
|
42
|
+
platform: undefined,
|
|
43
|
+
env() {
|
|
44
|
+
return undefined
|
|
45
|
+
},
|
|
46
|
+
passThrough() {},
|
|
47
|
+
waitUntil(promise) {
|
|
48
|
+
void promise
|
|
49
|
+
},
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
return response ?? new Response(null, { status: 404 })
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function runBasicUsageExample() {
|
|
57
|
+
const profiles = new Map([['42', { id: '42', name: 'Ada' }]])
|
|
58
|
+
|
|
59
|
+
const requestMiddleware = chain().use(ctx => ({
|
|
60
|
+
requestId: ctx.request.headers.get('x-request-id') ?? 'local',
|
|
61
|
+
}))
|
|
62
|
+
|
|
63
|
+
const handler = createRouter({ basePath: 'api/' })
|
|
64
|
+
.use(requestMiddleware)
|
|
65
|
+
.use(routes, {
|
|
66
|
+
profileRoute: {
|
|
67
|
+
GET(ctx) {
|
|
68
|
+
const profile = profiles.get(ctx.path.id)
|
|
69
|
+
if (!profile) {
|
|
70
|
+
return new Response('Profile not found', { status: 404 })
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
...profile,
|
|
74
|
+
includePosts: ctx.query.includePosts ?? false,
|
|
75
|
+
requestId: ctx.requestId,
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
PATCH(ctx) {
|
|
79
|
+
const current = profiles.get(ctx.path.id)
|
|
80
|
+
if (!current) {
|
|
81
|
+
return new Response('Profile not found', { status: 404 })
|
|
82
|
+
}
|
|
83
|
+
const profile = { ...current, name: ctx.body.name }
|
|
84
|
+
profiles.set(ctx.path.id, profile)
|
|
85
|
+
return {
|
|
86
|
+
...profile,
|
|
87
|
+
includePosts: false,
|
|
88
|
+
requestId: ctx.requestId,
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const client = createClient({
|
|
95
|
+
baseURL: 'https://example.test/api/',
|
|
96
|
+
routes,
|
|
97
|
+
headers: {
|
|
98
|
+
'content-type': 'application/json',
|
|
99
|
+
},
|
|
100
|
+
fetch: createLocalFetch(handler),
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
const fetched = await client.profileRoute.GET({
|
|
104
|
+
path: { id: '42' },
|
|
105
|
+
query: { includePosts: false },
|
|
106
|
+
headers: { 'x-request-id': 'docs' },
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
const updated = await client.profileRoute.PATCH({
|
|
110
|
+
path: { id: '42' },
|
|
111
|
+
body: { name: 'Grace' },
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
return { fetched, updated }
|
|
115
|
+
}
|
package/package.json
CHANGED
package/readme.md
DELETED
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
# rouzer
|
|
2
|
-
|
|
3
|
-
Type-safe routes shared by your server and client, powered by `zod` (input validation + transforms), `@remix-run/route-pattern` (URL matching), and `alien-middleware` (typed middleware chaining). The router output is intended to be used with `@hattip/core` adapters.
|
|
4
|
-
|
|
5
|
-
## Install
|
|
6
|
-
|
|
7
|
-
```sh
|
|
8
|
-
pnpm add rouzer zod
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
Everything is imported directly from `rouzer`.
|
|
12
|
-
|
|
13
|
-
## Define routes (shared)
|
|
14
|
-
|
|
15
|
-
```ts
|
|
16
|
-
// routes.ts
|
|
17
|
-
import * as z from 'zod'
|
|
18
|
-
import { $type, route } from 'rouzer'
|
|
19
|
-
|
|
20
|
-
export const helloRoute = route('hello/:name', {
|
|
21
|
-
GET: {
|
|
22
|
-
query: z.object({
|
|
23
|
-
excited: z.optional(z.boolean()),
|
|
24
|
-
}),
|
|
25
|
-
// The response is only type-checked at compile time.
|
|
26
|
-
response: $type<{ message: string }>(),
|
|
27
|
-
},
|
|
28
|
-
})
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
The following request parts can be validated with Zod:
|
|
32
|
-
|
|
33
|
-
- `path`
|
|
34
|
-
- `query`
|
|
35
|
-
- `body`
|
|
36
|
-
- `headers`
|
|
37
|
-
|
|
38
|
-
Zod validation happens on both the server and client.
|
|
39
|
-
|
|
40
|
-
## Route URL patterns
|
|
41
|
-
|
|
42
|
-
Rouzer uses `@remix-run/route-pattern` for matching and generation. Patterns can include:
|
|
43
|
-
|
|
44
|
-
- Pathname-only patterns like `blog/:slug` (default).
|
|
45
|
-
- Full URLs with protocol/hostname/port like `https://:store.shopify.com/orders`.
|
|
46
|
-
- Dynamic segments with `:param` names (valid JS identifiers), including multiple params in one segment like `v:major.:minor`.
|
|
47
|
-
- Optional segments wrapped in parentheses, which can be nested like `api(/v:major(.:minor))`.
|
|
48
|
-
- Wildcards with `*name` (captured) or `*` (uncaptured) for multi-segment paths like `assets/*path` or `files/*`.
|
|
49
|
-
- Query matching with `?` to require parameters or exact values like `search?q` or `search?q=routing`.
|
|
50
|
-
|
|
51
|
-
## Server router
|
|
52
|
-
|
|
53
|
-
```ts
|
|
54
|
-
import { chain, createRouter } from 'rouzer'
|
|
55
|
-
import { routes } from './routes'
|
|
56
|
-
|
|
57
|
-
const middlewares = chain().use(ctx => {
|
|
58
|
-
// An example middleware. For more info, see https://github.com/alien-rpc/alien-middleware#readme
|
|
59
|
-
return {
|
|
60
|
-
db: postgres(ctx.env('POSTGRES_URL')),
|
|
61
|
-
}
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
export const handler = createRouter({
|
|
65
|
-
debug: process.env.NODE_ENV === 'development',
|
|
66
|
-
})
|
|
67
|
-
.use(middlewares)
|
|
68
|
-
.use(routes, {
|
|
69
|
-
helloRoute: {
|
|
70
|
-
GET(ctx) {
|
|
71
|
-
const message = `Hello, ${ctx.path.name}${
|
|
72
|
-
ctx.query.excited ? '!' : '.'
|
|
73
|
-
}`
|
|
74
|
-
return { message }
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
})
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
## Router options
|
|
81
|
-
|
|
82
|
-
```ts
|
|
83
|
-
export const handler = createRouter({
|
|
84
|
-
basePath: 'api/',
|
|
85
|
-
cors: {
|
|
86
|
-
allowOrigins: [
|
|
87
|
-
'example.net',
|
|
88
|
-
'https://*.example.com',
|
|
89
|
-
'*://localhost:3000',
|
|
90
|
-
],
|
|
91
|
-
},
|
|
92
|
-
debug: process.env.NODE_ENV === 'development',
|
|
93
|
-
}).use(routes, {
|
|
94
|
-
helloRoute: {
|
|
95
|
-
GET(ctx) {
|
|
96
|
-
const message = `Hello, ${ctx.path.name}${ctx.query.excited ? '!' : '.'}`
|
|
97
|
-
return { message }
|
|
98
|
-
},
|
|
99
|
-
},
|
|
100
|
-
})
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
- `basePath` is prepended to every route (leading/trailing slashes are trimmed).
|
|
104
|
-
- CORS preflight (`OPTIONS`) is handled automatically for matched routes.
|
|
105
|
-
- `cors.allowOrigins` restricts preflight requests to a list of origins (default is to allow any origin).
|
|
106
|
-
- Wildcards are supported for protocol and subdomain; the protocol is optional and defaults to `https`.
|
|
107
|
-
- If you rely on `Cookie` or `Authorization` request headers, you must set
|
|
108
|
-
`Access-Control-Allow-Credentials` in your handler.
|
|
109
|
-
|
|
110
|
-
## Client wrapper
|
|
111
|
-
|
|
112
|
-
```ts
|
|
113
|
-
import { createClient } from 'rouzer'
|
|
114
|
-
import { helloRoute } from './routes'
|
|
115
|
-
|
|
116
|
-
const client = createClient({ baseURL: '/api/' })
|
|
117
|
-
|
|
118
|
-
const { message } = await client.json(
|
|
119
|
-
helloRoute.GET({ path: { name: 'world' }, query: { excited: true } })
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
// If you want the Response object, use `client.request` instead.
|
|
123
|
-
const response = await client.request(
|
|
124
|
-
helloRoute.GET({ path: { name: 'world' } })
|
|
125
|
-
)
|
|
126
|
-
|
|
127
|
-
const { message } = await response.json()
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
### Custom fetch
|
|
131
|
-
|
|
132
|
-
You can also pass a custom fetch implementation:
|
|
133
|
-
|
|
134
|
-
```ts
|
|
135
|
-
const client = createClient({
|
|
136
|
-
baseURL: '/api/',
|
|
137
|
-
fetch: myFetch,
|
|
138
|
-
})
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
### Shorthand route methods
|
|
142
|
-
|
|
143
|
-
Optionally pass your routes map to `createClient` to get per-route methods on the client:
|
|
144
|
-
|
|
145
|
-
```ts
|
|
146
|
-
import * as routes from './routes'
|
|
147
|
-
|
|
148
|
-
const client = createClient({
|
|
149
|
-
baseURL: '/api/',
|
|
150
|
-
routes, // <–– Pass the routes
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
// Shorthand methods now available:
|
|
154
|
-
await client.fooRoute.GET()
|
|
155
|
-
// …same as the longhand:
|
|
156
|
-
await client.json(routes.fooRoute.GET())
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
Routes that define a `response` type will call `client.json()` under the hood and return the parsed value; routes without one return the raw `Response`:
|
|
160
|
-
|
|
161
|
-
```ts
|
|
162
|
-
// helloRoute has a response schema, so you get the parsed payload
|
|
163
|
-
const { message } = await client.helloRoute.GET({
|
|
164
|
-
path: { name: 'world' },
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
// imagine pingRoute has no response schema; you get a Response object
|
|
168
|
-
const pingResponse = await client.pingRoute.GET({})
|
|
169
|
-
const pingText = await pingResponse.text()
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
## Add an endpoint
|
|
173
|
-
|
|
174
|
-
1. Declare it in `routes.ts` with `route(…)` and `zod` schemas.
|
|
175
|
-
2. Implement the handler in your router assembly with `createRouter(…).use(routes, { … })`.
|
|
176
|
-
3. Call it from the client with the generated helper via `client.json` or `client.request`.
|