rouzer 2.0.1 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -34
- package/dist/client/index.d.ts +29 -28
- package/dist/client/index.js +34 -16
- package/dist/http.d.ts +67 -0
- package/dist/http.js +44 -0
- package/dist/route.d.ts +19 -12
- package/dist/route.js +13 -8
- package/dist/server/router.d.ts +7 -3
- package/dist/server/router.js +35 -33
- package/dist/server/types.d.ts +25 -29
- package/dist/types.d.ts +26 -15
- package/docs/context.md +214 -42
- package/examples/basic-usage.ts +17 -16
- package/package.json +14 -10
package/dist/server/router.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { RoutePattern } from '@remix-run/route-pattern';
|
|
2
|
+
import { createMatcher } from '@remix-run/route-pattern/match';
|
|
2
3
|
import { chain, MiddlewareChain, } from 'alien-middleware';
|
|
3
4
|
import * as z from 'zod';
|
|
4
5
|
import { mapValues } from '../common.js';
|
|
@@ -13,14 +14,14 @@ class RouterObject extends MiddlewareChain {
|
|
|
13
14
|
this.basePath = config.basePath?.replace(/\/?$/, '/');
|
|
14
15
|
const allowOrigins = config.cors?.allowOrigins?.map(createOriginPattern);
|
|
15
16
|
if (allowOrigins) {
|
|
16
|
-
super.use((ctx) => {
|
|
17
|
+
super.use(((ctx) => {
|
|
17
18
|
const origin = ctx.request.headers.get('Origin');
|
|
18
19
|
if (origin &&
|
|
19
20
|
allowOrigins &&
|
|
20
21
|
!allowOrigins.some(pattern => pattern.test(origin))) {
|
|
21
22
|
return new Response(null, { status: 403 });
|
|
22
23
|
}
|
|
23
|
-
});
|
|
24
|
+
}));
|
|
24
25
|
}
|
|
25
26
|
}
|
|
26
27
|
use(...args) {
|
|
@@ -31,28 +32,13 @@ class RouterObject extends MiddlewareChain {
|
|
|
31
32
|
/** @internal */
|
|
32
33
|
useRoutes(routeSchemas, handlers) {
|
|
33
34
|
const { config, basePath } = this;
|
|
34
|
-
const routes =
|
|
35
|
-
name,
|
|
36
|
-
path: basePath
|
|
37
|
-
? new RoutePattern(route.path.source.replace(/^\/?/, basePath))
|
|
38
|
-
: route.path,
|
|
39
|
-
methods: mapValues(route.methods, (schema, method) => {
|
|
40
|
-
const handler = handlers[name][method];
|
|
41
|
-
if (!handler && config.debug) {
|
|
42
|
-
console.error(`Handler missing for route: ${method} ${name}`);
|
|
43
|
-
}
|
|
44
|
-
return {
|
|
45
|
-
schema,
|
|
46
|
-
handler,
|
|
47
|
-
};
|
|
48
|
-
}),
|
|
49
|
-
}));
|
|
35
|
+
const routes = flattenRoutes(routeSchemas, handlers, basePath ?? '', config.debug);
|
|
50
36
|
const addDebugHeaders = config.debug
|
|
51
37
|
? (context, route) => {
|
|
52
38
|
context.setHeader('X-Route-Name', route.name);
|
|
53
39
|
}
|
|
54
40
|
: null;
|
|
55
|
-
return super.use(async function (context) {
|
|
41
|
+
return super.use((async function (context) {
|
|
56
42
|
const request = context.request;
|
|
57
43
|
const origin = request.headers.get('Origin');
|
|
58
44
|
const url = (context.url ??= new URL(request.url));
|
|
@@ -65,28 +51,18 @@ class RouterObject extends MiddlewareChain {
|
|
|
65
51
|
'GET';
|
|
66
52
|
}
|
|
67
53
|
for (const route of routes) {
|
|
68
|
-
|
|
69
|
-
? route.methods[method]
|
|
70
|
-
: route.methods.ALL;
|
|
71
|
-
if (!props) {
|
|
54
|
+
if (route.method !== method) {
|
|
72
55
|
continue;
|
|
73
56
|
}
|
|
74
|
-
const { schema, handler } =
|
|
57
|
+
const { schema, handler } = route;
|
|
75
58
|
if (!handler) {
|
|
76
59
|
continue;
|
|
77
60
|
}
|
|
78
|
-
const match = route.
|
|
61
|
+
const match = route.matcher.match(url);
|
|
79
62
|
if (!match) {
|
|
80
63
|
continue;
|
|
81
64
|
}
|
|
82
65
|
if (isPreflight) {
|
|
83
|
-
const optionsHandler = handlers[route.name].OPTIONS;
|
|
84
|
-
if (optionsHandler) {
|
|
85
|
-
const response = await optionsHandler(context);
|
|
86
|
-
if (response) {
|
|
87
|
-
return response;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
66
|
return new Response(null, {
|
|
91
67
|
headers: {
|
|
92
68
|
'Access-Control-Allow-Origin': origin ?? '',
|
|
@@ -136,7 +112,7 @@ class RouterObject extends MiddlewareChain {
|
|
|
136
112
|
}
|
|
137
113
|
return Response.json(result);
|
|
138
114
|
}
|
|
139
|
-
});
|
|
115
|
+
}));
|
|
140
116
|
}
|
|
141
117
|
}
|
|
142
118
|
/**
|
|
@@ -153,6 +129,32 @@ export function createRouter(config = {}) {
|
|
|
153
129
|
Object.setPrototypeOf(handler, router);
|
|
154
130
|
return handler;
|
|
155
131
|
}
|
|
132
|
+
function flattenRoutes(tree, handlers, prefix, debug) {
|
|
133
|
+
const routes = [];
|
|
134
|
+
for (const [name, node] of Object.entries(tree)) {
|
|
135
|
+
if (node.kind === 'resource') {
|
|
136
|
+
routes.push(...flattenRoutes(node.children, handlers[name], joinPaths(prefix, node.path.source), debug));
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
const handler = handlers[name];
|
|
140
|
+
if (!handler && debug) {
|
|
141
|
+
console.error(`Handler missing for route: ${node.method} ${name}`);
|
|
142
|
+
}
|
|
143
|
+
routes.push({
|
|
144
|
+
name,
|
|
145
|
+
path: RoutePattern.parse(joinPaths(prefix, node.path?.source ?? '')),
|
|
146
|
+
matcher: createMatcher(joinPaths(prefix, node.path?.source ?? '')),
|
|
147
|
+
method: node.method,
|
|
148
|
+
schema: node.schema,
|
|
149
|
+
handler,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return routes;
|
|
154
|
+
}
|
|
155
|
+
function joinPaths(left, right) {
|
|
156
|
+
return [left, right].filter(Boolean).join('/').replace(/\/+/g, '/');
|
|
157
|
+
}
|
|
156
158
|
function httpClientError(error, message, config) {
|
|
157
159
|
return Response.json({
|
|
158
160
|
...error,
|
package/dist/server/types.d.ts
CHANGED
|
@@ -1,46 +1,42 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { MatchParams } from '@remix-run/route-pattern/match';
|
|
2
2
|
import type { AnyMiddlewareChain, MiddlewareChain, MiddlewareContext } from 'alien-middleware';
|
|
3
3
|
import type * as z from 'zod';
|
|
4
4
|
import { Promisable } from '../common.js';
|
|
5
|
-
import type {
|
|
5
|
+
import type { HttpAction, HttpResource, HttpRouteTree } from '../http.js';
|
|
6
|
+
import type { InferRouteResponse, RouteSchema } from '../types.js';
|
|
6
7
|
type RequestContext<TMiddleware extends AnyMiddlewareChain> = MiddlewareContext<TMiddleware>;
|
|
7
8
|
type RouteRequestHandler<TMiddleware extends AnyMiddlewareChain, TArgs extends object, TResult> = (context: RequestContext<TMiddleware> & TArgs) => Promisable<TResult | Response>;
|
|
8
|
-
type
|
|
9
|
-
path:
|
|
9
|
+
type InferActionHandler<TMiddleware extends AnyMiddlewareChain, TAction extends HttpAction, TPath extends string> = TAction['method'] extends 'GET' ? RouteRequestHandler<TMiddleware, {
|
|
10
|
+
path: TAction['schema'] extends {
|
|
10
11
|
path: any;
|
|
11
|
-
} ? z.infer<
|
|
12
|
-
query:
|
|
12
|
+
} ? z.infer<TAction['schema']['path']> : MatchParams<TPath>;
|
|
13
|
+
query: TAction['schema'] extends {
|
|
13
14
|
query: any;
|
|
14
|
-
} ? z.infer<
|
|
15
|
-
headers:
|
|
15
|
+
} ? z.infer<TAction['schema']['query']> : undefined;
|
|
16
|
+
headers: TAction['schema'] extends {
|
|
16
17
|
headers: any;
|
|
17
|
-
} ? z.infer<
|
|
18
|
-
}, InferRouteResponse<
|
|
19
|
-
path:
|
|
18
|
+
} ? z.infer<TAction['schema']['headers']> : undefined;
|
|
19
|
+
}, InferRouteResponse<Extract<TAction['schema'], RouteSchema>>> : RouteRequestHandler<TMiddleware, {
|
|
20
|
+
path: TAction['schema'] extends {
|
|
20
21
|
path: any;
|
|
21
|
-
} ? z.infer<
|
|
22
|
-
body:
|
|
22
|
+
} ? z.infer<TAction['schema']['path']> : MatchParams<TPath>;
|
|
23
|
+
body: TAction['schema'] extends {
|
|
23
24
|
body: any;
|
|
24
|
-
} ? z.infer<
|
|
25
|
-
headers:
|
|
25
|
+
} ? z.infer<TAction['schema']['body']> : undefined;
|
|
26
|
+
headers: TAction['schema'] extends {
|
|
26
27
|
headers: any;
|
|
27
|
-
} ? z.infer<
|
|
28
|
-
}, InferRouteResponse<
|
|
28
|
+
} ? z.infer<TAction['schema']['headers']> : undefined;
|
|
29
|
+
}, InferRouteResponse<Extract<TAction['schema'], RouteSchema>>>;
|
|
30
|
+
type Join<A extends string, B extends string> = A extends '' ? B : B extends '' ? A : `${A}/${B}`;
|
|
29
31
|
/**
|
|
30
32
|
* Handler map shape required by `createRouter().use(routes, handlers)`.
|
|
31
33
|
*
|
|
32
|
-
* @remarks
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
34
|
+
* @remarks The handler object mirrors the HTTP route tree. Resource nodes become
|
|
35
|
+
* nested handler objects, while action nodes become direct handler functions.
|
|
36
|
+
* Handler context is inferred from middleware plus accumulated path params,
|
|
37
|
+
* query/body schemas, and header schemas.
|
|
36
38
|
*/
|
|
37
|
-
export type RouteRequestHandlerMap<TRoutes extends
|
|
38
|
-
[K in keyof TRoutes]:
|
|
39
|
-
[TMethod in keyof TRoutes[K]['methods']]: InferRouteRequestHandler<TMiddleware, Extract<TRoutes[K]['methods'][TMethod], RouteSchema>, Extract<TMethod, string>, TRoutes[K]['path']['source']>;
|
|
40
|
-
} & {
|
|
41
|
-
OPTIONS?: RouteRequestHandler<TMiddleware, {
|
|
42
|
-
path: Params<TRoutes[K]['path']['source']>;
|
|
43
|
-
}, void>;
|
|
44
|
-
};
|
|
39
|
+
export type RouteRequestHandlerMap<TRoutes extends HttpRouteTree = HttpRouteTree, TMiddleware extends AnyMiddlewareChain = MiddlewareChain, TPrefix extends string = ''> = {
|
|
40
|
+
[K in keyof TRoutes]: TRoutes[K] extends HttpResource<infer P, infer C> ? RouteRequestHandlerMap<C, TMiddleware, Join<TPrefix, P>> : TRoutes[K] extends HttpAction<infer P, any, any> ? InferActionHandler<TMiddleware, TRoutes[K], Join<TPrefix, P>> : never;
|
|
45
41
|
};
|
|
46
42
|
export {};
|
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { RoutePattern } from '@remix-run/route-pattern';
|
|
2
|
+
import type { MatchParams } from '@remix-run/route-pattern/match';
|
|
2
3
|
import * as z from 'zod';
|
|
3
4
|
import { Unchecked } from './common.js';
|
|
4
5
|
/**
|
|
@@ -35,11 +36,11 @@ export type MutationRouteSchema = {
|
|
|
35
36
|
response?: Unchecked<any>;
|
|
36
37
|
};
|
|
37
38
|
/**
|
|
38
|
-
* Method schema map accepted by `route(...)
|
|
39
|
+
* Method schema map accepted by the low-level `route(...)` helper.
|
|
39
40
|
*
|
|
40
|
-
* @remarks `GET` validates query input
|
|
41
|
-
* input
|
|
42
|
-
*
|
|
41
|
+
* @remarks `GET` validates query input and mutation methods validate JSON body
|
|
42
|
+
* input. Prefer `rouzer/http` actions for route trees registered with
|
|
43
|
+
* `createRouter().use(...)` or `createClient({ routes })`.
|
|
43
44
|
*/
|
|
44
45
|
export type RouteSchemaMap = {
|
|
45
46
|
GET?: QueryRouteSchema;
|
|
@@ -62,7 +63,13 @@ export type RouteSchemaMap = {
|
|
|
62
63
|
};
|
|
63
64
|
/** Any route method schema Rouzer can execute. */
|
|
64
65
|
export type RouteSchema = QueryRouteSchema | MutationRouteSchema;
|
|
65
|
-
/**
|
|
66
|
+
/**
|
|
67
|
+
* Low-level route map shape produced from `route(...)` declarations.
|
|
68
|
+
*
|
|
69
|
+
* @remarks The router and client shorthand registration APIs now expect
|
|
70
|
+
* `HttpRouteTree` values from the `rouzer/http` subpath. Use this type only for
|
|
71
|
+
* code that still works directly with low-level `route(...)` descriptors.
|
|
72
|
+
*/
|
|
66
73
|
export type Routes = {
|
|
67
74
|
[key: string]: {
|
|
68
75
|
path: RoutePattern;
|
|
@@ -78,7 +85,7 @@ type PathArgs<T, P extends string> = T extends {
|
|
|
78
85
|
[K in keyof T as 'path']?: z.infer<TPath>;
|
|
79
86
|
} : {
|
|
80
87
|
[K in keyof T as 'path']: z.infer<TPath>;
|
|
81
|
-
} :
|
|
88
|
+
} : MatchParams<P> extends infer TParams ? {} extends TParams ? {
|
|
82
89
|
[K in keyof T as 'path']?: TParams;
|
|
83
90
|
} : {
|
|
84
91
|
[K in keyof T as 'path']: TParams;
|
|
@@ -100,7 +107,8 @@ type MutationArgs<T> = T extends MutationRouteSchema ? T extends {
|
|
|
100
107
|
body?: unknown;
|
|
101
108
|
} : unknown;
|
|
102
109
|
/**
|
|
103
|
-
* Arguments accepted by a
|
|
110
|
+
* Arguments accepted by a request factory such as an HTTP action's `.request(...)`
|
|
111
|
+
* or a low-level `route.GET(...)` factory.
|
|
104
112
|
*
|
|
105
113
|
* @remarks The type is derived from a method schema and route pattern. `path`,
|
|
106
114
|
* `query`, `body`, and `headers` are validated by the client before `fetch` when
|
|
@@ -117,7 +125,7 @@ export type RouteArgs<T extends RouteSchema = any, P extends string = string> =
|
|
|
117
125
|
headers?: Record<string, string | undefined>;
|
|
118
126
|
};
|
|
119
127
|
/**
|
|
120
|
-
* Request descriptor produced by
|
|
128
|
+
* Request descriptor produced by an HTTP action or route request factory.
|
|
121
129
|
*
|
|
122
130
|
* @remarks Pass this object to `client.request(...)` for a raw `Response` or
|
|
123
131
|
* `client.json(...)` for parsed JSON handling.
|
|
@@ -149,23 +157,26 @@ type InferRouteArgsBody<TArgs> = TArgs extends {
|
|
|
149
157
|
body?: infer TBody;
|
|
150
158
|
} ? TBody : never;
|
|
151
159
|
/**
|
|
152
|
-
* Infer the request body type from a
|
|
160
|
+
* Infer the request body type from a schema or request factory.
|
|
153
161
|
*
|
|
154
|
-
* @remarks
|
|
155
|
-
*
|
|
162
|
+
* @remarks HTTP action schemas can be inspected with
|
|
163
|
+
* `InferRouteBody<typeof action.schema>`. Request factories for mutation methods
|
|
164
|
+
* infer their `body` argument type. Schemas without a body schema infer
|
|
165
|
+
* `unknown`.
|
|
156
166
|
*/
|
|
157
167
|
export type InferRouteBody<T> = T extends RouteRequestFactory<any, any> ? InferRouteArgsBody<T['$args']> : T extends RouteSchema ? InferRouteSchemaBody<T> : never;
|
|
158
168
|
/**
|
|
159
|
-
* Infer the request body type for a named method on a `Route`.
|
|
169
|
+
* Infer the request body type for a named method on a low-level `Route`.
|
|
160
170
|
*
|
|
161
171
|
* @remarks `GET` and `ALL` infer `never` because they do not accept request
|
|
162
|
-
* bodies.
|
|
172
|
+
* bodies. For `rouzer/http` actions, prefer
|
|
173
|
+
* `InferRouteBody<typeof action.schema>`.
|
|
163
174
|
*/
|
|
164
175
|
export type InferRouteMethodBody<TRoute extends {
|
|
165
176
|
methods: RouteSchemaMap;
|
|
166
177
|
}, TMethod extends keyof TRoute['methods']> = TMethod extends 'GET' | 'ALL' ? never : TMethod extends keyof TRoute ? InferRouteBody<TRoute[TMethod]> : InferRouteBody<Extract<TRoute['methods'][TMethod], RouteSchema>>;
|
|
167
178
|
/**
|
|
168
|
-
* Callable factory attached to
|
|
179
|
+
* Callable factory attached to an HTTP action or low-level `Route` method.
|
|
169
180
|
*
|
|
170
181
|
* @remarks Calling a factory validates no data by itself; it creates a typed
|
|
171
182
|
* `RouteRequest` descriptor for `createClient` to validate and send.
|
package/docs/context.md
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# Rouzer context
|
|
2
2
|
|
|
3
|
-
Rouzer is for applications that want one route
|
|
4
|
-
server and the client that calls it. A route
|
|
5
|
-
HTTP method schemas, and
|
|
3
|
+
Rouzer is for applications that want one TypeScript HTTP route tree to drive
|
|
4
|
+
both the server and the client that calls it. A route tree combines URL
|
|
5
|
+
patterns, named actions, HTTP method schemas, and optional compile-time response
|
|
6
|
+
types.
|
|
6
7
|
|
|
7
8
|
## When to use Rouzer
|
|
8
9
|
|
|
@@ -13,7 +14,7 @@ Use Rouzer when:
|
|
|
13
14
|
- request validation should run before server handlers and before client `fetch`
|
|
14
15
|
calls
|
|
15
16
|
- a Hattip-compatible handler fits your server runtime
|
|
16
|
-
- generated clients should stay close to
|
|
17
|
+
- generated clients should stay close to route definitions instead of being
|
|
17
18
|
produced by a separate OpenAPI build step
|
|
18
19
|
|
|
19
20
|
Rouzer is not a response validation library, an OpenAPI generator, or a complete
|
|
@@ -22,43 +23,113 @@ small client wrapper.
|
|
|
22
23
|
|
|
23
24
|
## Core abstractions
|
|
24
25
|
|
|
25
|
-
###
|
|
26
|
+
### HTTP route trees
|
|
26
27
|
|
|
27
|
-
Declare routes with `
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
Declare shared routes with the `rouzer/http` subpath:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { $type } from 'rouzer'
|
|
32
|
+
import * as http from 'rouzer/http'
|
|
33
|
+
|
|
34
|
+
export const getProfile = http.get('profiles/:id', {
|
|
35
|
+
response: $type<Profile>(),
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
export const routes = { getProfile }
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
An action is a callable endpoint leaf. Use `http.get`, `http.post`, `http.put`,
|
|
42
|
+
`http.patch`, or `http.del`/`http.delete` to declare one HTTP operation. The key
|
|
43
|
+
you put the action under is the client and handler name; the action path is the
|
|
44
|
+
URL pattern.
|
|
45
|
+
|
|
46
|
+
Use `http.resource(path, children)` when several actions share a path prefix or
|
|
47
|
+
when you want nested client/handler namespaces:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
export const profiles = http.resource('profiles/:id', {
|
|
51
|
+
get: http.get({
|
|
52
|
+
response: $type<Profile>(),
|
|
53
|
+
}),
|
|
54
|
+
update: http.patch({
|
|
55
|
+
body: updateProfileSchema,
|
|
56
|
+
response: $type<Profile>(),
|
|
57
|
+
}),
|
|
58
|
+
posts: http.resource('posts', {
|
|
59
|
+
list: http.get({
|
|
60
|
+
response: $type<Post[]>(),
|
|
61
|
+
}),
|
|
62
|
+
}),
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
export const routes = { profiles }
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Resource property names do not affect the URL. Resource paths and action-local
|
|
69
|
+
paths are joined, so the examples above expose `profiles/:id`, `profiles/:id`,
|
|
70
|
+
and `profiles/:id/posts`. Path params from parent resources are accumulated into
|
|
71
|
+
child action types.
|
|
72
|
+
|
|
73
|
+
Patterns are parsed by `@remix-run/route-pattern` v0.21. Params can be inferred
|
|
74
|
+
from patterns such as `hello/:name`, `v:major.:minor`,
|
|
75
|
+
`api(/v:major(.:minor))`, `assets/*path`, and `search?q`. Full URL patterns such
|
|
76
|
+
as `https://:store.shopify.com/orders` are supported for top-level actions; keep
|
|
77
|
+
them out of resource/base-path composition.
|
|
78
|
+
|
|
79
|
+
### Method schemas
|
|
32
80
|
|
|
33
81
|
Method schemas describe the request pieces Rouzer should validate:
|
|
34
82
|
|
|
35
|
-
|
|
|
36
|
-
|
|
|
37
|
-
| `
|
|
38
|
-
| `
|
|
39
|
-
| `ALL` | `path`, `query`, `headers` | Fallback when the incoming method is not explicitly declared. No body or response type. |
|
|
83
|
+
| Action helper | Request schemas | Notes |
|
|
84
|
+
| ------------------------------------- | -------------------------------------- | ---------------- |
|
|
85
|
+
| `http.get(...)` | `path`, `query`, `headers`, `response` | No request body. |
|
|
86
|
+
| `http.post/put/patch/delete/del(...)` | `path`, `body`, `headers`, `response` | No query schema. |
|
|
40
87
|
|
|
41
88
|
If you omit a `path` schema, TypeScript infers path params from the pattern and
|
|
42
89
|
server handlers receive them as strings. Add a Zod `path` schema when you need
|
|
43
90
|
runtime validation, transforms, or non-string handler types.
|
|
44
91
|
|
|
92
|
+
The HTTP action API models explicit operations. It does not expose the old
|
|
93
|
+
method-map `ALL` fallback route shape; declare the concrete methods your client
|
|
94
|
+
and server support.
|
|
95
|
+
|
|
45
96
|
### `$type<T>()`
|
|
46
97
|
|
|
47
98
|
`response: $type<T>()` is a TypeScript-only marker. It tells handlers and client
|
|
48
|
-
|
|
99
|
+
action functions what response payload type to expect, but Rouzer does not
|
|
49
100
|
validate response bodies at runtime.
|
|
50
101
|
|
|
51
|
-
|
|
52
|
-
|
|
102
|
+
Actions without a `response` marker return a raw `Response` from client action
|
|
103
|
+
functions. Actions with a `response` marker use `client.json(...)` under the hood
|
|
53
104
|
and return parsed JSON typed as `T`.
|
|
54
105
|
|
|
55
106
|
### Router
|
|
56
107
|
|
|
57
108
|
`createRouter()` returns a Hattip-compatible handler. Use `.use(middleware)` to
|
|
58
109
|
append typed `alien-middleware` middleware and `.use(routes, handlers)` to attach
|
|
59
|
-
route
|
|
110
|
+
an HTTP route tree.
|
|
111
|
+
|
|
112
|
+
The handler object mirrors the route tree:
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
createRouter().use(routes, {
|
|
116
|
+
profiles: {
|
|
117
|
+
get(ctx) {
|
|
118
|
+
return loadProfile(ctx.path.id)
|
|
119
|
+
},
|
|
120
|
+
update(ctx) {
|
|
121
|
+
return updateProfile(ctx.path.id, ctx.body)
|
|
122
|
+
},
|
|
123
|
+
posts: {
|
|
124
|
+
list(ctx) {
|
|
125
|
+
return listPosts(ctx.path.id)
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
})
|
|
130
|
+
```
|
|
60
131
|
|
|
61
|
-
Handlers receive a context typed from middleware plus the
|
|
132
|
+
Handlers receive a context typed from middleware plus the action schema:
|
|
62
133
|
|
|
63
134
|
- `GET` handlers receive `ctx.path`, `ctx.query`, and `ctx.headers`
|
|
64
135
|
- mutation handlers receive `ctx.path`, `ctx.body`, and `ctx.headers`
|
|
@@ -66,7 +137,7 @@ Handlers receive a context typed from middleware plus the route schema:
|
|
|
66
137
|
- plain values are returned with `Response.json(value)`
|
|
67
138
|
- return a `Response` when you need custom status, headers, or body handling
|
|
68
139
|
|
|
69
|
-
`basePath` is prepended to route
|
|
140
|
+
`basePath` is prepended to route tree paths, `debug` adds matched-route debug
|
|
70
141
|
headers and more detailed validation errors, and `cors.allowOrigins` restricts
|
|
71
142
|
requests with an `Origin` header.
|
|
72
143
|
|
|
@@ -74,12 +145,14 @@ requests with an `Origin` header.
|
|
|
74
145
|
|
|
75
146
|
`createClient({ baseURL, routes })` creates:
|
|
76
147
|
|
|
77
|
-
- `client.request(
|
|
78
|
-
|
|
79
|
-
-
|
|
80
|
-
|
|
148
|
+
- `client.request(action.request(args))` for a raw `Response` when the action
|
|
149
|
+
request factory contains the full path you want to call
|
|
150
|
+
- `client.json(action.request(args))` for parsed JSON and default non-2xx
|
|
151
|
+
throwing
|
|
152
|
+
- a client tree that mirrors `routes`, with action functions such as
|
|
153
|
+
`client.profiles.get(args)` when `routes` is supplied
|
|
81
154
|
|
|
82
|
-
Prefer an absolute `baseURL` for
|
|
155
|
+
Prefer an absolute `baseURL` for generated client URLs:
|
|
83
156
|
|
|
84
157
|
```ts
|
|
85
158
|
const client = createClient({
|
|
@@ -92,12 +165,23 @@ Default headers can be supplied with `headers`, per-request headers are merged o
|
|
|
92
165
|
top, and a custom `fetch` implementation can be supplied for tests or non-browser
|
|
93
166
|
runtimes.
|
|
94
167
|
|
|
168
|
+
### Low-level `route(...)` descriptors
|
|
169
|
+
|
|
170
|
+
The root package still exports `route(pattern, methods)`. It creates method-keyed
|
|
171
|
+
request descriptor factories such as `legacyRoute.GET(args)` for explicit
|
|
172
|
+
`client.request(...)` or `client.json(...)` calls.
|
|
173
|
+
|
|
174
|
+
Prefer `rouzer/http` route trees for shared server/client routing. The router and
|
|
175
|
+
client shorthand registration APIs expect `HttpAction`/`HttpResource` trees, not
|
|
176
|
+
the older method-map objects produced by `route(...)`.
|
|
177
|
+
|
|
95
178
|
## Lifecycle
|
|
96
179
|
|
|
97
|
-
1. Define shared
|
|
98
|
-
2. Attach
|
|
99
|
-
3. Create a client with the same route
|
|
100
|
-
4. Client calls validate `path`, `query`, `body`, and `headers` before
|
|
180
|
+
1. Define shared HTTP actions/resources with `rouzer/http` and Zod schemas.
|
|
181
|
+
2. Attach that route tree to a server with `createRouter().use(routes, handlers)`.
|
|
182
|
+
3. Create a client with the same route tree.
|
|
183
|
+
4. Client action calls validate `path`, `query`, `body`, and `headers` before
|
|
184
|
+
`fetch`.
|
|
101
185
|
5. The router matches the request, validates the matched inputs, and calls the
|
|
102
186
|
handler.
|
|
103
187
|
6. Plain handler results become JSON responses; explicit `Response` objects pass
|
|
@@ -112,20 +196,51 @@ string-coercion step.
|
|
|
112
196
|
|
|
113
197
|
### Choose a client call style
|
|
114
198
|
|
|
115
|
-
Use
|
|
199
|
+
Use client action functions for normal application calls:
|
|
116
200
|
|
|
117
201
|
```ts
|
|
118
|
-
await client.
|
|
202
|
+
await client.profiles.get({ path: { id: '42' } })
|
|
203
|
+
await client.profiles.update({
|
|
204
|
+
path: { id: '42' },
|
|
205
|
+
body: { name: 'Ada' },
|
|
206
|
+
})
|
|
119
207
|
```
|
|
120
208
|
|
|
121
|
-
Use longhand calls when you need to choose response handling explicitly
|
|
209
|
+
Use longhand calls when you need to choose response handling explicitly. The
|
|
210
|
+
action request factory must include the full path you want to call, so this style
|
|
211
|
+
is most convenient for top-level actions:
|
|
122
212
|
|
|
123
213
|
```ts
|
|
214
|
+
export const getProfile = http.get('profiles/:id', {
|
|
215
|
+
response: $type<Profile>(),
|
|
216
|
+
})
|
|
217
|
+
export const routes = { getProfile }
|
|
218
|
+
|
|
124
219
|
const response = await client.request(
|
|
125
|
-
routes.
|
|
220
|
+
routes.getProfile.request({ path: { id: '42' } })
|
|
126
221
|
)
|
|
127
222
|
|
|
128
|
-
const json = await client.json(
|
|
223
|
+
const json = await client.json(
|
|
224
|
+
routes.getProfile.request({ path: { id: '42' } })
|
|
225
|
+
)
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Group resource actions
|
|
229
|
+
|
|
230
|
+
Use resources when the public API reads better as a tree or when actions share
|
|
231
|
+
path params:
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
export const organizations = http.resource('orgs/:orgId', {
|
|
235
|
+
members: http.resource('members/:memberId', {
|
|
236
|
+
get: http.get({ response: $type<Member>() }),
|
|
237
|
+
remove: http.delete({}),
|
|
238
|
+
}),
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
await client.organizations.members.get({
|
|
242
|
+
path: { orgId: 'acme', memberId: '42' },
|
|
243
|
+
})
|
|
129
244
|
```
|
|
130
245
|
|
|
131
246
|
### Return custom responses
|
|
@@ -142,16 +257,69 @@ is JSON, its properties are copied onto the thrown `Error`.
|
|
|
142
257
|
`client.json(...)` as-is; Rouzer does not automatically parse a returned
|
|
143
258
|
`Response` from `onJsonError`.
|
|
144
259
|
|
|
260
|
+
### Update code written for v2.0.1
|
|
261
|
+
|
|
262
|
+
Rouzer now uses action/resource route trees for router registration and client
|
|
263
|
+
shorthands. A v2.0.1 method-map route such as this:
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
export const profileRoute = route('profiles/:id', {
|
|
267
|
+
GET: { response: $type<Profile>() },
|
|
268
|
+
PATCH: { body: updateProfileSchema, response: $type<Profile>() },
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
export const routes = { profileRoute }
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
becomes a named action tree:
|
|
275
|
+
|
|
276
|
+
```ts
|
|
277
|
+
import * as http from 'rouzer/http'
|
|
278
|
+
|
|
279
|
+
export const profiles = http.resource('profiles/:id', {
|
|
280
|
+
get: http.get({ response: $type<Profile>() }),
|
|
281
|
+
update: http.patch({
|
|
282
|
+
body: updateProfileSchema,
|
|
283
|
+
response: $type<Profile>(),
|
|
284
|
+
}),
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
export const routes = { profiles }
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Handler maps and client calls mirror the new action names:
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
createRouter().use(routes, {
|
|
294
|
+
profiles: {
|
|
295
|
+
get(ctx) {
|
|
296
|
+
return loadProfile(ctx.path.id)
|
|
297
|
+
},
|
|
298
|
+
update(ctx) {
|
|
299
|
+
return updateProfile(ctx.path.id, ctx.body)
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
await client.profiles.get({ path: { id: '42' } })
|
|
305
|
+
await client.profiles.update({
|
|
306
|
+
path: { id: '42' },
|
|
307
|
+
body: { name: 'Ada' },
|
|
308
|
+
})
|
|
309
|
+
```
|
|
310
|
+
|
|
145
311
|
## Patterns to prefer
|
|
146
312
|
|
|
147
|
-
- Export route
|
|
148
|
-
|
|
313
|
+
- Export route trees from a small shared module and import that module on both
|
|
314
|
+
server and client.
|
|
315
|
+
- Use `rouzer/http` actions for routes that are registered with
|
|
316
|
+
`createRouter().use(...)` or `createClient({ routes })`.
|
|
149
317
|
- Add Zod schemas when you need runtime guarantees; rely on inferred path params
|
|
150
318
|
only when string params are sufficient.
|
|
151
319
|
- Use `response: $type<T>()` for JSON endpoints that should have typed client
|
|
152
|
-
|
|
153
|
-
-
|
|
154
|
-
`
|
|
320
|
+
action functions.
|
|
321
|
+
- Name actions after domain operations (`get`, `list`, `update`, `archive`) and
|
|
322
|
+
let `http.get/post/put/patch/delete` own the transport method.
|
|
155
323
|
- Set `content-type: application/json` yourself when your server or middleware
|
|
156
324
|
depends on that header.
|
|
157
325
|
|
|
@@ -159,9 +327,13 @@ is JSON, its properties are copied onto the thrown `Error`.
|
|
|
159
327
|
|
|
160
328
|
- `$type<T>()` is compile-time only and does not validate response payloads.
|
|
161
329
|
- Pathname route patterns expect an absolute client `baseURL`.
|
|
330
|
+
- Resource and action keys are API names only; paths come from the pattern
|
|
331
|
+
strings passed to `http.resource(...)` and action helpers.
|
|
332
|
+
- Nested action `.request(...)` factories do not include parent resource paths;
|
|
333
|
+
prefer client action functions for nested resources.
|
|
162
334
|
- Extra `RequestInit` fields in route args, such as `signal` or `credentials`,
|
|
163
335
|
are accepted by the type surface but are not forwarded by `createClient`.
|
|
164
|
-
-
|
|
165
|
-
|
|
336
|
+
- The HTTP action API has no `ALL` fallback route. Declare explicit actions for
|
|
337
|
+
supported methods.
|
|
166
338
|
- Rouzer does not automatically set `Access-Control-Allow-Credentials`; set it in
|
|
167
339
|
your handler when credentialed cross-origin requests need it.
|