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.
@@ -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 = Object.entries(routeSchemas).map(([name, route]) => ({
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
- const props = route.methods.hasOwnProperty(method)
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 } = props;
57
+ const { schema, handler } = route;
75
58
  if (!handler) {
76
59
  continue;
77
60
  }
78
- const match = route.path.match(url);
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,
@@ -1,46 +1,42 @@
1
- import type { Params } from '@remix-run/route-pattern';
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 { InferRouteResponse, Routes, RouteSchema } from '../types.js';
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 InferRouteRequestHandler<TMiddleware extends AnyMiddlewareChain, TSchema extends RouteSchema, TMethod extends string, TPath extends string> = TMethod extends 'GET' ? RouteRequestHandler<TMiddleware, {
9
- path: TSchema extends {
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<TSchema['path']> : Params<TPath>;
12
- query: TSchema extends {
12
+ } ? z.infer<TAction['schema']['path']> : MatchParams<TPath>;
13
+ query: TAction['schema'] extends {
13
14
  query: any;
14
- } ? z.infer<TSchema['query']> : undefined;
15
- headers: TSchema extends {
15
+ } ? z.infer<TAction['schema']['query']> : undefined;
16
+ headers: TAction['schema'] extends {
16
17
  headers: any;
17
- } ? z.infer<TSchema['headers']> : undefined;
18
- }, InferRouteResponse<TSchema>> : RouteRequestHandler<TMiddleware, {
19
- path: TSchema extends {
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<TSchema['path']> : Params<TPath>;
22
- body: TSchema extends {
22
+ } ? z.infer<TAction['schema']['path']> : MatchParams<TPath>;
23
+ body: TAction['schema'] extends {
23
24
  body: any;
24
- } ? z.infer<TSchema['body']> : undefined;
25
- headers: TSchema extends {
25
+ } ? z.infer<TAction['schema']['body']> : undefined;
26
+ headers: TAction['schema'] extends {
26
27
  headers: any;
27
- } ? z.infer<TSchema['headers']> : undefined;
28
- }, InferRouteResponse<TSchema>>;
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 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.
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 Routes = Routes, TMiddleware extends AnyMiddlewareChain = MiddlewareChain> = {
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 { Params, RoutePattern } from '@remix-run/route-pattern';
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, mutation methods validate JSON body
41
- * input, and `ALL` acts as a fallback for methods that are not declared
42
- * explicitly.
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
- /** Route map accepted by `createRouter().use(...)` and `createClient(...)`. */
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
- } : Params<P> extends infer TParams ? {} extends TParams ? {
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 route request factory such as `route.GET(...)`.
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 a route request factory.
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 mutation schema or route request factory.
160
+ * Infer the request body type from a schema or request factory.
153
161
  *
154
- * @remarks Route request factories for mutation methods infer their `body`
155
- * argument type. Mutation schemas without a body schema infer `unknown`.
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 a `Route` for each declared method.
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 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.
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 the route definitions instead of being
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
- ### Route declarations
26
+ ### HTTP route trees
26
27
 
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`.
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
- | 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. |
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
- shorthand methods what response payload type to expect, but Rouzer does not
99
+ action functions what response payload type to expect, but Rouzer does not
49
100
  validate response bodies at runtime.
50
101
 
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
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 handlers.
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 route schema:
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 patterns, `debug` adds matched-route debug
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(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
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 pathname route patterns:
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 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`.
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 shorthand methods for normal application calls:
199
+ Use client action functions for normal application calls:
116
200
 
117
201
  ```ts
118
- await client.helloRoute.GET({ path: { name: 'Ada' } })
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.helloRoute.GET({ path: { name: 'Ada' } })
220
+ routes.getProfile.request({ path: { id: '42' } })
126
221
  )
127
222
 
128
- const json = await client.json(routes.helloRoute.GET({ path: { name: 'Ada' } }))
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 declarations from a small shared module and import that module on
148
- both server and client.
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
- shorthand methods.
153
- - Use explicit HTTP methods when you want precise handler context types; reserve
154
- `ALL` for true fallback behavior.
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
- - `ALL` can declare `query`, but handler context typing is less precise than
165
- explicit `GET` handlers.
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.