rouzer 3.2.0 → 4.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 CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  Rouzer lets you declare an HTTP route tree once and share its TypeScript types
4
4
  and Zod validation between a Hattip-compatible server and a typed fetch client.
5
+ The client is always created from that route tree.
5
6
 
6
7
  ## What it does
7
8
 
@@ -45,7 +46,7 @@ Consider something else if:
45
46
  - Zod v4 or newer
46
47
  - a Hattip adapter when using `createRouter(...)`
47
48
  - a Fetch API implementation when using `createClient(...)`
48
- - an absolute `baseURL` for generated client URLs
49
+ - an absolute `baseURL` and shared `routes` tree for generated client URLs
49
50
 
50
51
  ## Installation
51
52
 
@@ -101,9 +102,9 @@ const { message } = await client.hello({
101
102
  })
102
103
  ```
103
104
 
104
- `handler` can be mounted with any Hattip adapter. Client action calls validate
105
- route arguments before `fetch`; server handlers validate matched path, query,
106
- headers, and JSON bodies before your handler runs.
105
+ `handler` can be mounted with any Hattip adapter. Generated client action calls
106
+ validate route arguments before `fetch`; server handlers validate matched path,
107
+ query, headers, and JSON bodies before your handler runs.
107
108
 
108
109
  ### Typed status responses
109
110
 
@@ -2,7 +2,6 @@ import { Promisable } from '../common.js';
2
2
  import type { HttpAction, HttpResource, HttpRouteTree } from '../http.js';
3
3
  import { type ClientResponsePlugin } from '../response.js';
4
4
  import type { RouteArgs } from '../types/args.js';
5
- import type { RouteRequest } from '../types/request.js';
6
5
  import type { InferRouteResponse } from '../types/response.js';
7
6
  import type { RouteSchema } from '../types/schema.js';
8
7
  /** Client type inferred from an HTTP route tree passed to `createClient`. */
@@ -10,9 +9,8 @@ export type RouzerClient<TRoutes extends HttpRouteTree = Record<string, never>>
10
9
  /**
11
10
  * Create a typed fetch client for an HTTP route tree.
12
11
  *
13
- * @remarks The returned client always includes `request(...)` for raw responses
14
- * and `json(...)` for parsed JSON. Passing `routes` also mirrors the resource
15
- * tree and attaches direct action functions such as `client.users.list(...)`.
12
+ * @remarks The returned client mirrors the resource tree and attaches direct
13
+ * action functions such as `client.users.list(...)`.
16
14
  */
17
15
  export declare function createClient<TRoutes extends HttpRouteTree = Record<string, never>>(config: {
18
16
  /**
@@ -38,22 +36,22 @@ export declare function createClient<TRoutes extends HttpRouteTree = Record<stri
38
36
  * await client.users.list({ query: { page: 1 } })
39
37
  * ```
40
38
  */
41
- routes?: TRoutes;
39
+ routes: TRoutes;
42
40
  /** Response codec plugins used by generated action functions. */
43
41
  plugins?: readonly ClientResponsePlugin[];
44
42
  /**
45
- * Custom handler for non-2xx responses from `.json()` and generated response
46
- * helpers.
43
+ * Custom handler for non-2xx responses from generated client action
44
+ * functions.
47
45
  *
48
- * @remarks When provided, the return value is returned from the response
49
- * helper as-is; Rouzer does not automatically parse a `Response` returned by
50
- * this hook.
46
+ * @remarks When provided, the return value is returned from the client action
47
+ * as-is; Rouzer does not automatically parse a `Response` returned by this
48
+ * hook.
51
49
  */
52
50
  onJsonError?: (response: Response) => Promisable<unknown>;
53
51
  /** Custom `fetch` implementation to use for requests. */
54
52
  fetch?: typeof globalThis.fetch;
55
53
  }): ClientTree<TRoutes, ""> & {
56
- config: {
54
+ clientConfig: {
57
55
  /**
58
56
  * Absolute base URL used for generated request URLs.
59
57
  *
@@ -77,25 +75,21 @@ export declare function createClient<TRoutes extends HttpRouteTree = Record<stri
77
75
  * await client.users.list({ query: { page: 1 } })
78
76
  * ```
79
77
  */
80
- routes?: TRoutes;
78
+ routes: TRoutes;
81
79
  /** Response codec plugins used by generated action functions. */
82
80
  plugins?: readonly ClientResponsePlugin[];
83
81
  /**
84
- * Custom handler for non-2xx responses from `.json()` and generated response
85
- * helpers.
82
+ * Custom handler for non-2xx responses from generated client action
83
+ * functions.
86
84
  *
87
- * @remarks When provided, the return value is returned from the response
88
- * helper as-is; Rouzer does not automatically parse a `Response` returned by
89
- * this hook.
85
+ * @remarks When provided, the return value is returned from the client action
86
+ * as-is; Rouzer does not automatically parse a `Response` returned by this
87
+ * hook.
90
88
  */
91
89
  onJsonError?: (response: Response) => Promisable<unknown>;
92
90
  /** Custom `fetch` implementation to use for requests. */
93
91
  fetch?: typeof globalThis.fetch;
94
92
  };
95
- request: <T extends RouteRequest>({ path: pathBuilder, method, args, schema, }: T) => Promise<Response & {
96
- json(): Promise<T['$result']>;
97
- }>;
98
- json: <T extends RouteRequest>(props: T) => Promise<T['$result']>;
99
93
  };
100
94
  type Join<A extends string, B extends string> = A extends '' ? B : B extends '' ? A : `${A}/${B}`;
101
95
  /** Client object shape produced from an HTTP route tree. */
@@ -6,18 +6,15 @@ import { getResponseMapPluginIds, isErrorMarker, isResponseMap, } from '../respo
6
6
  /**
7
7
  * Create a typed fetch client for an HTTP route tree.
8
8
  *
9
- * @remarks The returned client always includes `request(...)` for raw responses
10
- * and `json(...)` for parsed JSON. Passing `routes` also mirrors the resource
11
- * tree and attaches direct action functions such as `client.users.list(...)`.
9
+ * @remarks The returned client mirrors the resource tree and attaches direct
10
+ * action functions such as `client.users.list(...)`.
12
11
  */
13
12
  export function createClient(config) {
14
13
  const baseURL = config.baseURL.replace(/\/?$/, '/');
15
14
  const defaultHeaders = config.headers && shake(config.headers);
16
15
  const fetch = config.fetch ?? globalThis.fetch;
17
16
  const responsePlugins = createResponsePluginMap(config.plugins, 'client response');
18
- if (config.routes) {
19
- validateClientResponsePlugins(config.routes, responsePlugins);
20
- }
17
+ validateClientResponsePlugins(config.routes, responsePlugins);
21
18
  async function request({ path: pathBuilder, method, args, schema, }) {
22
19
  let { path, query, body, headers, ...init } = args;
23
20
  if (schema.path) {
@@ -61,13 +58,6 @@ export function createClient(config) {
61
58
  headers: (headers ?? defaultHeaders),
62
59
  });
63
60
  }
64
- async function json(props) {
65
- const response = await request(props);
66
- if (!response.ok) {
67
- return handleResponseError(response, props);
68
- }
69
- return response.json();
70
- }
71
61
  async function response(props) {
72
62
  const httpResponse = await request(props);
73
63
  const responseSchema = props.schema.response;
@@ -127,12 +117,8 @@ export function createClient(config) {
127
117
  throw error;
128
118
  }
129
119
  return {
130
- ...(config.routes
131
- ? connectTree(config.routes, '', request, response)
132
- : null),
133
- config,
134
- request,
135
- json,
120
+ ...connectTree(config.routes, '', request, response),
121
+ clientConfig: config,
136
122
  };
137
123
  }
138
124
  function connectTree(tree, prefix, request, response) {
package/dist/http.d.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { RoutePattern } from '@remix-run/route-pattern';
2
- import type { RouteRequestFactory } from './types/request.js';
3
2
  import type { RouteSchema } from './types/schema.js';
4
3
  /** HTTP methods supported by Rouzer action declarations. */
5
4
  export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
@@ -18,8 +17,6 @@ export type HttpAction<P extends string = string, T extends RouteSchema = RouteS
18
17
  method: M;
19
18
  /** Request validation and optional response type schema. */
20
19
  schema: T;
21
- /** Low-level request descriptor factory for this action. */
22
- request: RouteRequestFactory<T, P>;
23
20
  };
24
21
  /**
25
22
  * Path-scoped namespace in an HTTP route tree.
package/dist/http.js CHANGED
@@ -34,12 +34,5 @@ function action(method, pathOrSchema, schema) {
34
34
  ? RoutePattern.parse(pathOrSchema)
35
35
  : undefined;
36
36
  schema ??= typeof pathOrSchema === 'string' ? {} : pathOrSchema;
37
- const request = ((args = {}) => ({
38
- schema,
39
- path: path ?? RoutePattern.parse(''),
40
- method,
41
- args,
42
- $result: undefined,
43
- }));
44
- return { kind: 'action', path, method, schema, request };
37
+ return { kind: 'action', path, method, schema };
45
38
  }
@@ -1,5 +1,7 @@
1
1
  import type { Promisable } from './common.js';
2
- import type { RouteRequest } from './types/request.js';
2
+ import type { RoutePattern } from '@remix-run/route-pattern';
3
+ import type { RouteArgs } from './types/args.js';
4
+ import type { RouteSchema } from './types/schema.js';
3
5
  /** Runtime key carried by response plugin markers. */
4
6
  export declare const responsePluginMarker: unique symbol;
5
7
  /**
@@ -24,9 +26,16 @@ export type ClientResponsePlugin = {
24
26
  /** Decode a successful `Response` into the client action result. */
25
27
  decode(response: Response, context: {
26
28
  marker: ResponsePluginMarker<any, any>;
27
- request: RouteRequest;
29
+ request: ClientResponsePluginRequest;
28
30
  }): Promisable<unknown>;
29
31
  };
32
+ /** Request metadata passed to client response plugins. */
33
+ export type ClientResponsePluginRequest = {
34
+ schema: RouteSchema;
35
+ path: RoutePattern;
36
+ method: string;
37
+ args: RouteArgs;
38
+ };
30
39
  /** Router-side response plugin used by `createRouter({ plugins })`. */
31
40
  export type RouterResponsePlugin = {
32
41
  /** Stable response codec id matched against route response markers. */
@@ -32,7 +32,7 @@ type MutationArgs<T> = T extends MutationRouteSchema ? T extends {
32
32
  body?: unknown;
33
33
  } : unknown;
34
34
  /**
35
- * Arguments accepted by a client action function or low-level request factory.
35
+ * Arguments accepted by a generated client action function.
36
36
  *
37
37
  * @remarks The type is derived from an action schema and route pattern. `path`,
38
38
  * `query`, `body`, and `headers` are validated by the client before `fetch` when
@@ -1,7 +1,6 @@
1
1
  export type * from './args.js';
2
2
  export type * from './handler.js';
3
3
  export type * from './infer.js';
4
- export type * from './request.js';
5
4
  export type * from './response.js';
6
5
  export type * from './schema.js';
7
6
  export type * from './server.js';
@@ -1,19 +1,14 @@
1
1
  import type * as z from 'zod';
2
2
  import type { MutationRouteSchema, RouteSchema } from './schema.js';
3
- import type { RouteRequestFactory } from './request.js';
4
3
  type InferRouteSchemaBody<TSchema> = TSchema extends MutationRouteSchema ? TSchema extends {
5
4
  body: infer TBody;
6
5
  } ? z.infer<TBody> : unknown : never;
7
- type InferRouteArgsBody<TArgs> = TArgs extends {
8
- body?: infer TBody;
9
- } ? TBody : never;
10
6
  /**
11
- * Infer the request body type from an action schema or request factory.
7
+ * Infer the request body type from an action schema.
12
8
  *
13
9
  * @remarks HTTP action schemas can be inspected with
14
- * `InferRouteBody<typeof action.schema>`. Request factories for mutation actions
15
- * infer their `body` argument type. Schemas without a body schema infer
10
+ * `InferRouteBody<typeof action.schema>`. Schemas without a body schema infer
16
11
  * `unknown`.
17
12
  */
18
- export type InferRouteBody<T> = T extends RouteRequestFactory<any, any> ? InferRouteArgsBody<T['$args']> : T extends RouteSchema ? InferRouteSchemaBody<T> : never;
13
+ export type InferRouteBody<T> = T extends RouteSchema ? InferRouteSchemaBody<T> : never;
19
14
  export {};
package/docs/context.md CHANGED
@@ -211,18 +211,16 @@ requests with an `Origin` header.
211
211
 
212
212
  ### Client
213
213
 
214
- `createClient({ baseURL, routes })` creates:
215
-
216
- - `client.request(action.request(args))` for a raw `Response` when the action
217
- request factory contains the full path you want to call
218
- - `client.json(action.request(args))` for parsed JSON and default non-2xx
219
- throwing
220
- - response-map support for generated client action functions, returning
221
- `[error, value, status]` tuples for declared statuses
222
- - response plugin support for generated client action functions, such as
223
- `ndjson.clientPlugin` for NDJSON response streams
224
- - a client tree that mirrors `routes`, with action functions such as
225
- `client.profiles.get(args)` when `routes` is supplied
214
+ `createClient({ baseURL, routes })` creates a client tree that mirrors
215
+ `routes`, with action functions such as `client.profiles.get(args)`.
216
+ Generated action functions include:
217
+
218
+ - raw `Response` results for actions without a response schema
219
+ - parsed JSON and default non-2xx throwing for `$type<T>()` responses
220
+ - response-map support, returning `[error, value, status]` tuples for declared
221
+ statuses
222
+ - response plugin support, such as `ndjson.clientPlugin` for NDJSON response
223
+ streams
226
224
 
227
225
  Prefer an absolute `baseURL` for generated client URLs:
228
226
 
@@ -235,7 +233,8 @@ const client = createClient({
235
233
 
236
234
  Default headers can be supplied with `headers`, per-request headers are merged on
237
235
  top, and a custom `fetch` implementation can be supplied for tests or non-browser
238
- runtimes.
236
+ runtimes. The returned client exposes the original options as `clientConfig`, so
237
+ route actions named `config` remain available as `client.config(...)`.
239
238
 
240
239
  ## Lifecycle
241
240
 
@@ -260,9 +259,9 @@ string-coercion step.
260
259
 
261
260
  ## Common tasks
262
261
 
263
- ### Choose a client call style
262
+ ### Call client actions
264
263
 
265
- Use client action functions for normal application calls:
264
+ Use generated client action functions for application calls:
266
265
 
267
266
  ```ts
268
267
  await client.profiles.get({ path: { id: '42' } })
@@ -272,29 +271,6 @@ await client.profiles.update({
272
271
  })
273
272
  ```
274
273
 
275
- Use longhand calls when you need to choose response handling explicitly. The
276
- action request factory must include the full path you want to call, so this style
277
- is most convenient for top-level actions:
278
-
279
- ```ts
280
- export const getProfile = http.get('profiles/:id', {
281
- response: $type<Profile>(),
282
- })
283
- export const routes = { getProfile }
284
-
285
- const response = await client.request(
286
- routes.getProfile.request({ path: { id: '42' } })
287
- )
288
-
289
- const json = await client.json(
290
- routes.getProfile.request({ path: { id: '42' } })
291
- )
292
- ```
293
-
294
- Response maps and response plugins are applied by generated client action
295
- functions. For longhand calls to mapped or plugin-backed routes, use
296
- `client.request(...)` for the raw `Response` and decode the response yourself.
297
-
298
274
  ### Handle declared error responses
299
275
 
300
276
  Use `$error<T>()` inside a response map when an error status is part of the route
@@ -420,7 +396,7 @@ custom headers. Return a plain value for the default `Response.json(value)` path
420
396
 
421
397
  ### Customize JSON errors
422
398
 
423
- By default, `client.json(...)` and generated client action functions throw for
399
+ By default, generated client action functions throw for
424
400
  non-2xx responses that are not declared in a response map. If the response body
425
401
  is JSON, its properties are copied onto the thrown `Error`.
426
402
 
@@ -484,7 +460,7 @@ await client.profiles.update({
484
460
  - Export route trees from a small shared module and import that module on both
485
461
  server and client.
486
462
  - Use `rouzer/http` actions for routes that are registered with
487
- `createRouter().use(...)` or `createClient({ routes })`.
463
+ `createRouter().use(...)` or the required `createClient({ routes })` option.
488
464
  - Add Zod schemas when you need runtime guarantees; rely on inferred path params
489
465
  only when string params are sufficient.
490
466
  - Use `response: $type<T>()` for JSON endpoints that should have typed client
@@ -513,11 +489,10 @@ await client.profiles.update({
513
489
  - Pathname route patterns expect an absolute client `baseURL`.
514
490
  - Resource and action keys are API names only; paths come from the pattern
515
491
  strings passed to `http.resource(...)` and action helpers.
516
- - Nested action `.request(...)` factories do not include parent resource paths;
517
- prefer client action functions for nested resources.
518
492
  - Extra `RequestInit` fields in route args, such as `signal` or `credentials`,
519
- are forwarded by `createClient`; `method`, `body`, and `headers` are reserved
520
- for Rouzer's action metadata and validated call arguments.
493
+ are forwarded by `createClient`; `method` and `body` are reserved for Rouzer's
494
+ action metadata and validated call arguments. Use route args or client defaults
495
+ for request headers.
521
496
  - The HTTP action API has no `ALL` fallback route. Declare explicit actions for
522
497
  supported methods.
523
498
  - Rouzer does not automatically set `Access-Control-Allow-Credentials`; set it in
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rouzer",
3
- "version": "3.2.0",
3
+ "version": "4.0.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -1,35 +0,0 @@
1
- import type { RoutePattern } from '@remix-run/route-pattern';
2
- import type { RouteArgs } from './args.js';
3
- import type { InferRouteResponse } from './response.js';
4
- import type { RouteSchema } from './schema.js';
5
- /**
6
- * Request descriptor produced by an HTTP action request factory.
7
- *
8
- * @remarks Pass this object to `client.request(...)` for a raw `Response` or
9
- * `client.json(...)` for parsed JSON handling.
10
- */
11
- export type RouteRequest<TResult = any> = {
12
- /** Method schema used for client-side validation. */
13
- schema: RouteSchema;
14
- /** Parsed route pattern used to generate the request URL. */
15
- path: RoutePattern;
16
- /** HTTP method to send. */
17
- method: string;
18
- /** Validated route arguments and request options. */
19
- args: RouteArgs;
20
- /** Phantom result type consumed by `client.json(...)`. */
21
- $result: TResult;
22
- };
23
- /**
24
- * Callable factory attached to an HTTP action.
25
- *
26
- * @remarks Calling a factory validates no data by itself; it creates a typed
27
- * `RouteRequest` descriptor for `createClient` to validate and send.
28
- */
29
- export type RouteRequestFactory<T extends RouteSchema, P extends string> = {
30
- (...p: RouteArgs<T, P> extends infer TArgs ? {} extends TArgs ? [args?: TArgs] : [args: TArgs] : never): RouteRequest<InferRouteResponse<T>>;
31
- /** Inferred argument type for this request factory. */
32
- $args: RouteArgs<T, P>;
33
- /** Inferred response type for this request factory. */
34
- $response: InferRouteResponse<T>;
35
- };
@@ -1 +0,0 @@
1
- export {};