rouzer 5.3.0 → 6.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
@@ -1,278 +1,160 @@
1
1
  # Rouzer
2
2
 
3
- Rouzer lets you declare an HTTP route tree once and share its TypeScript types
4
- and Zod validation between a Hattip-compatible server and a typed fetch client.
5
- The client is always created from that route tree.
3
+ Rouzer is a small TypeScript HTTP framework for projects that can share one
4
+ route tree between server and client code. The route tree defines URL patterns,
5
+ named actions, request validation, and response contracts once, then Rouzer uses
6
+ it to build a fetch-compatible router and a typed fetch client.
6
7
 
7
- ## What it does
8
+ Rouzer also includes a chainable middleware layer for request-scoped state,
9
+ host/runtime data, background work, response callbacks, and adapter helpers that
10
+ turn a router into a Fetch API handler.
8
11
 
9
- A Rouzer HTTP route tree defines URL patterns, named actions, method schemas, and
10
- optional JSON, error, or newline-delimited JSON response types once, then reuses
11
- that contract to:
12
+ ## Use It When
12
13
 
13
- - validate client arguments before `fetch`
14
- - match and validate server requests before handlers run
15
- - type handler context from path, query/body, headers, and middleware
16
- - attach typed client action functions such as `client.profiles.get(...)`
17
- - send JSON object request bodies or raw `BodyInit` payloads
18
- - parse typed JSON responses, declared error responses, and NDJSON streams
14
+ Use Rouzer when:
19
15
 
20
- Rouzer optimizes for shared TypeScript route modules over language-agnostic API
21
- schemas or generated SDKs.
16
+ - your server and client can import the same TypeScript route module
17
+ - you want Zod validation before client requests and before server handlers
18
+ - a fetch-compatible handler fits your runtime or adapter
19
+ - named resource/action calls are a better fit than generated SDK classes
22
20
 
23
- ## Is this for you?
21
+ Consider another tool when you need OpenAPI-first schemas, generated clients for
22
+ other languages, server-side response validation for every handler return, or a
23
+ framework that owns rendering, deployment, and data loading.
24
24
 
25
- Use Rouzer if:
26
-
27
- - your server and client can import the same TypeScript route tree
28
- - you want Zod request validation on both sides of an HTTP boundary
29
- - response data is validated at data/client boundaries, not by re-checking every
30
- handler return
31
- - a Hattip-compatible handler fits your server runtime
32
- - you prefer named resource/action functions over a generated client class
33
-
34
- Consider something else if:
35
-
36
- - you need OpenAPI-first workflows, schema files, or generated clients for other
37
- languages
38
- - you want the router to validate every response body at the server boundary;
39
- `$type<T>()`, `$error<T>()`, and `ndjson.$type<T>()` are type contracts
40
- - you want a framework that owns controllers, data loading, rendering, and
41
- deployment adapters
42
- - you cannot use ESM or Zod v4+
43
-
44
- ## Requirements
45
-
46
- - ESM runtime and tooling
47
- - Zod v4 or newer
48
- - a Hattip adapter when using `createRouter(...)`
49
- - a Fetch API implementation when using `createClient(...)`
50
- - an absolute `baseURL` and shared `routes` tree for generated client URLs
51
-
52
- ## Installation
25
+ ## Install
53
26
 
54
27
  ```sh
55
28
  pnpm add rouzer zod
56
29
  ```
57
30
 
58
- Import the primary API from the root package and declare routes through the HTTP
59
- subpath:
31
+ Rouzer requires ESM, Zod v4 or newer, a Fetch API implementation for clients,
32
+ and a fetch-compatible server or adapter for routers.
60
33
 
61
34
  ```ts
62
- import { $error, $type, chain, createClient, createRouter, metadata } from 'rouzer'
35
+ import {
36
+ $error,
37
+ $type,
38
+ chain,
39
+ createClient,
40
+ createRouter,
41
+ metadata,
42
+ toFetchHandler,
43
+ } from 'rouzer'
63
44
  import * as http from 'rouzer/http'
45
+ import * as ndjson from 'rouzer/ndjson'
64
46
  ```
65
47
 
66
- `chain` is re-exported from `alien-middleware` for typed server middleware.
67
-
68
- ## Quick example
48
+ `chain`, `toFetchHandler`, request context types, and related middleware helpers
49
+ are part of the root Rouzer API.
69
50
 
70
- This example shows the core loop: one HTTP action contract defines validation,
71
- server handler types, and the typed client call.
51
+ ## Small Example
72
52
 
73
53
  ```ts
74
54
  import * as z from 'zod'
75
- import { $type, createClient, createRouter } from 'rouzer'
55
+ import {
56
+ $type,
57
+ chain,
58
+ createClient,
59
+ createRouter,
60
+ toFetchHandler,
61
+ } from 'rouzer'
76
62
  import * as http from 'rouzer/http'
77
63
 
78
- export const hello = http.get('hello/:name', {
79
- query: z.object({
80
- excited: z.optional(z.boolean()),
81
- }),
82
- response: $type<{ message: string }>(),
83
- })
84
-
85
- export const routes = { hello }
86
-
87
- export const handler = createRouter({ basePath: 'api/' }).use(routes, {
88
- hello(ctx) {
89
- return {
90
- message: `Hello, ${ctx.path.name}${ctx.query.excited ? '!' : '.'}`,
91
- }
92
- },
93
- })
94
-
95
- const client = createClient({
96
- baseURL: 'https://example.com/api/',
97
- routes,
98
- })
99
-
100
- const { message } = await client.hello({
101
- name: 'world',
102
- excited: true,
103
- })
104
- ```
105
-
106
- `handler` can be mounted with any Hattip adapter. Generated client action calls
107
- validate flat route arguments before `fetch`; server handlers validate matched
108
- path, query, headers, and JSON bodies before your handler runs. Per-request
109
- headers, abort signals, and other `RequestInit` options are passed as a second
110
- client action argument. Routes declared with `body: http.rawBody()` pass a
111
- `BodyInit` payload through to `fetch` without JSON encoding.
112
-
113
- ### Typed status responses
114
-
115
- Use a response map when client code needs declared error statuses as data instead
116
- of exceptions.
117
-
118
- ```ts
119
- import { $error, $type, createClient, createRouter } from 'rouzer'
120
- import * as http from 'rouzer/http'
121
-
122
- type User = { id: string; name: string }
123
- type NotFound = { code: 'NOT_FOUND'; message: string }
124
-
125
- export const getUser = http.get('users/:id', {
126
- response: {
127
- 200: $type<User>(),
128
- 404: $error<NotFound>(),
129
- },
130
- })
131
- export const routes = { getUser }
132
-
133
- createRouter().use(routes, {
134
- getUser(ctx) {
135
- if (ctx.path.id === 'missing') {
136
- return ctx.error(404, {
137
- code: 'NOT_FOUND',
138
- message: 'User not found',
139
- })
140
- }
141
- return { id: ctx.path.id, name: 'Ada' }
142
- },
143
- })
144
-
145
- const client = createClient({
146
- baseURL: 'https://example.com/api/',
147
- routes,
148
- })
149
-
150
- const [error, user, status] = await client.getUser({ id: '42' })
151
- ```
152
-
153
- Success entries resolve as `[null, value, status]`; declared error entries
154
- resolve as `[error, null, status]`.
155
-
156
- ### Raw request bodies
157
-
158
- Use `http.rawBody()` when an action needs to send a `BodyInit` payload such as a
159
- `Blob`, `Uint8Array`, `ReadableStream`, `FormData`, or string without JSON
160
- encoding.
161
-
162
- ```ts
163
- export const uploadAvatar = http.post('profiles/:id/avatar', {
164
- body: http.rawBody(),
165
- })
166
-
167
- await client.uploadAvatar({ id: '42' }, { body: file })
168
- ```
169
-
170
- For raw-body routes without path or query input, the generated client accepts the
171
- body as the first argument:
172
-
173
- ```ts
174
- export const upload = http.post('upload', {
175
- body: http.rawBody(),
176
- })
177
-
178
- await client.upload(file, { headers: { 'content-type': file.type } })
179
- ```
180
-
181
- Server handlers for raw-body routes read from `ctx.request` directly with Fetch
182
- APIs such as `arrayBuffer()`, `blob()`, `formData()`, or `text()`.
183
-
184
- ### Route metadata
185
-
186
- Use `metadata(...)` to attach optional runtime metadata to HTTP resources or
187
- actions. Metadata does not affect routing, validation, client typing, or handler
188
- behavior; it is preserved on route nodes for generated clients, CLIs, docs, and
189
- route inspectors.
64
+ type Profile = {
65
+ id: string
66
+ name: string
67
+ requestId: string
68
+ }
190
69
 
191
- ```ts
192
- export const sessions = http.resource('sessions', {
193
- ...metadata({
194
- description: 'Daemon-managed session control.',
195
- }),
196
- list: http.post('list', {
197
- ...metadata({
198
- description: 'Lists daemon-managed sessions and pagination state.',
70
+ export const profiles = http.resource('profiles/:id', {
71
+ get: http.get({
72
+ query: z.object({
73
+ includePosts: z.optional(z.boolean()),
199
74
  }),
200
- response: $type<SessionList>(),
75
+ response: $type<Profile>(),
201
76
  }),
202
77
  })
203
- ```
204
78
 
205
- The constructed nodes expose metadata as `node.metadata`.
79
+ export const routes = { profiles }
206
80
 
207
- ### Client lifecycle hooks
81
+ const requestInfo = chain().use(ctx => ({
82
+ requestId: ctx.request.headers.get('x-request-id') ?? 'local',
83
+ }))
208
84
 
209
- Pass `clientHook` to observe generated client action calls without wrapping the
210
- client tree:
85
+ const router = createRouter({ basePath: 'api/' })
86
+ .use(requestInfo)
87
+ .use(routes, {
88
+ profiles: {
89
+ get(ctx) {
90
+ return {
91
+ id: ctx.path.id,
92
+ name: 'Ada',
93
+ requestId: ctx.requestId,
94
+ }
95
+ },
96
+ },
97
+ })
98
+
99
+ const fetchHandler = toFetchHandler(router)
211
100
 
212
- ```ts
213
101
  const client = createClient({
214
102
  baseURL: 'https://example.com/api/',
215
103
  routes,
216
- clientHook(event) {
217
- if (event.type === 'request.success') {
218
- console.log(event.routeName, event.durationMs)
219
- }
220
- },
221
104
  })
222
- ```
223
105
 
224
- Rouzer emits `request.start` before client-side validation, then
225
- `request.success` when the action resolves or `request.error` when it rejects.
226
- Terminal events include the parsed response or thrown error plus `durationMs`.
227
- Hook errors are swallowed.
106
+ const profile = await client.profiles.get(
107
+ { id: '42', includePosts: false },
108
+ { headers: { 'x-request-id': 'docs' } }
109
+ )
110
+ ```
228
111
 
229
- ### NDJSON response streams
112
+ The shared `routes` object drives the handler map and the generated client. The
113
+ middleware output becomes part of the handler context, while the route schema
114
+ adds typed `ctx.path`, `ctx.query`, `ctx.body`, and `ctx.headers` values.
230
115
 
231
- Use `response: ndjson.$type<T>()` for endpoints that stream
232
- newline-delimited JSON. Add `ndjson.routerPlugin` to the router and
233
- `ndjson.clientPlugin` to the client. Handlers return an `Iterable<T>` or
234
- `AsyncIterable<T>`; Rouzer wraps it in an `application/x-ndjson` response.
235
- Client action functions resolve to an `AsyncIterable<T>`.
116
+ ## Core Concepts
236
117
 
237
- ```ts
238
- import { createClient, createRouter } from 'rouzer'
239
- import * as http from 'rouzer/http'
240
- import * as ndjson from 'rouzer/ndjson'
118
+ Route contracts live in shared TypeScript modules. Use `rouzer/http` resources
119
+ for path-scoped namespaces and actions such as `http.get`, `http.post`, and
120
+ `http.patch` for concrete HTTP operations.
241
121
 
242
- export const events = http.get('events', {
243
- response: ndjson.$type<{ id: number; message: string }>(),
244
- })
245
- export const routes = { events }
122
+ Routers are chainable request handlers. `createRouter()` returns a handler;
123
+ `.use(middleware)` appends middleware and `.use(routes, handlers)` attaches a
124
+ route tree.
246
125
 
247
- createRouter({ plugins: [ndjson.routerPlugin] }).use(routes, {
248
- async *events() {
249
- yield { id: 1, message: 'ready' }
250
- },
251
- })
126
+ Handlers mirror the route tree. Rouzer validates the matched request before the
127
+ handler runs, then lets the handler return JSON data, a custom `Response`,
128
+ declared status helpers, or a response-plugin value such as an NDJSON stream.
252
129
 
253
- const client = createClient({
254
- baseURL: 'https://example.com/api/',
255
- routes,
256
- plugins: [ndjson.clientPlugin],
257
- })
258
- for await (const event of await client.events()) {
259
- console.log(event.message)
260
- }
261
- ```
130
+ Clients mirror the same route tree. `createClient({ baseURL, routes })` returns
131
+ typed action functions with flat route input objects and optional per-request
132
+ `RequestInit` options.
262
133
 
263
- If a client aborts the request signal or stops iteration early by breaking from
264
- `for await` or calling the iterator's `return()`, Rouzer cancels the response
265
- body and calls the server source iterator's `return()`. Sources that wait for
266
- future events should make those waits abort-aware when they need cleanup to run
267
- while an awaited operation is still pending.
134
+ Response markers are type contracts. `$type<T>()`, `$error<T>()`, and
135
+ `ndjson.$type<T>()` shape handler and client types, but they do not re-validate
136
+ handler return values at the server boundary.
268
137
 
269
138
  ## Documentation
270
139
 
271
- - [Concepts, API selection, v5 client input notes, and migration notes](docs/context.md)
272
- - [Runnable shared-route example](examples/basic-usage.ts)
273
- - [Runnable typed error response example](examples/error-responses.ts)
274
- - [Runnable NDJSON response-stream example](examples/ndjson-stream.ts)
275
- - Generated declarations in the published package provide the exact signatures
276
- for every public export, including the `rouzer/http` and `rouzer/ndjson`
277
- subpaths.
278
- - Public TSDoc in `src/` owns symbol-level behavior and option details.
140
+ - [Documentation map](docs/index.md)
141
+ - [Framework concepts](docs/concepts.md)
142
+ - [Route contracts](docs/routes.md)
143
+ - [Middleware and request context](docs/middleware.md)
144
+ - [Routers and handlers](docs/handlers.md)
145
+ - [Typed client](docs/client.md)
146
+ - [Responses, errors, and plugins](docs/responses.md)
147
+ - [NDJSON streaming](docs/streaming.md)
148
+ - [Runtime and adapters](docs/runtime.md)
149
+ - [Patterns, constraints, and migrations](docs/patterns.md)
150
+ - [Migration from v5 to v6](docs/migration-v5-to-v6.md)
151
+
152
+ Runnable examples:
153
+
154
+ - [Basic shared-route example](examples/basic-usage.ts)
155
+ - [Typed status response example](examples/error-responses.ts)
156
+ - [NDJSON response-stream example](examples/ndjson-stream.ts)
157
+
158
+ Published declarations provide exact signatures for every public export,
159
+ including the `rouzer/http` and `rouzer/ndjson` subpaths. Public TSDoc in `src/`
160
+ owns symbol-level behavior and option details.
package/dist/http.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { RoutePattern } from '@remix-run/route-pattern';
2
- import { type RouteMetadata, type RouteMetadataMarker } from './metadata.js';
2
+ import { type RouteMetadata } from './metadata.js';
3
3
  import type { RawBodySchema, RouteSchema } from './types/schema.js';
4
4
  /** HTTP methods supported by Rouzer action declarations. */
5
5
  export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
@@ -43,7 +43,6 @@ export type HttpNode = HttpAction | HttpResource;
43
43
  export type HttpRouteTree = {
44
44
  [key: string]: HttpNode;
45
45
  };
46
- type RouteDeclaration<T extends object> = T & Partial<RouteMetadataMarker>;
47
46
  type StringKeys<T> = Pick<T, Extract<keyof T, string>>;
48
47
  /**
49
48
  * Declare an HTTP resource namespace.
@@ -52,22 +51,22 @@ type StringKeys<T> = Pick<T, Extract<keyof T, string>>;
52
51
  * property names are API names only; they do not affect the URL unless the child
53
52
  * is another resource or an action with an explicit path.
54
53
  */
55
- export declare function resource<const P extends string, const TChildren extends HttpRouteTree>(path: P, children: RouteDeclaration<TChildren>): HttpResource<P, StringKeys<TChildren>>;
54
+ export declare function resource<const P extends string, const TChildren extends HttpRouteTree>(path: P, children: TChildren): HttpResource<P, StringKeys<TChildren>>;
56
55
  /** Declare a GET action, optionally with an action-local path segment. */
57
- export declare function get<const P extends string, const T extends RouteSchema>(path: P, schema: RouteDeclaration<T>): HttpAction<P, T, 'GET'>;
58
- export declare function get<const T extends RouteSchema>(schema: RouteDeclaration<T>): HttpAction<'', T, 'GET'>;
56
+ export declare function get<const P extends string, const T extends RouteSchema>(path: P, schema: T): HttpAction<P, T, 'GET'>;
57
+ export declare function get<const T extends RouteSchema>(schema: T): HttpAction<'', T, 'GET'>;
59
58
  /** Declare a POST action, optionally with an action-local path segment. */
60
- export declare function post<const P extends string, const T extends RouteSchema>(path: P, schema: RouteDeclaration<T>): HttpAction<P, T, 'POST'>;
61
- export declare function post<const T extends RouteSchema>(schema: RouteDeclaration<T>): HttpAction<'', T, 'POST'>;
59
+ export declare function post<const P extends string, const T extends RouteSchema>(path: P, schema: T): HttpAction<P, T, 'POST'>;
60
+ export declare function post<const T extends RouteSchema>(schema: T): HttpAction<'', T, 'POST'>;
62
61
  /** Declare a PUT action, optionally with an action-local path segment. */
63
- export declare function put<const P extends string, const T extends RouteSchema>(path: P, schema: RouteDeclaration<T>): HttpAction<P, T, 'PUT'>;
64
- export declare function put<const T extends RouteSchema>(schema: RouteDeclaration<T>): HttpAction<'', T, 'PUT'>;
62
+ export declare function put<const P extends string, const T extends RouteSchema>(path: P, schema: T): HttpAction<P, T, 'PUT'>;
63
+ export declare function put<const T extends RouteSchema>(schema: T): HttpAction<'', T, 'PUT'>;
65
64
  /** Declare a PATCH action, optionally with an action-local path segment. */
66
- export declare function patch<const P extends string, const T extends RouteSchema>(path: P, schema: RouteDeclaration<T>): HttpAction<P, T, 'PATCH'>;
67
- export declare function patch<const T extends RouteSchema>(schema: RouteDeclaration<T>): HttpAction<'', T, 'PATCH'>;
65
+ export declare function patch<const P extends string, const T extends RouteSchema>(path: P, schema: T): HttpAction<P, T, 'PATCH'>;
66
+ export declare function patch<const T extends RouteSchema>(schema: T): HttpAction<'', T, 'PATCH'>;
68
67
  /** Declare a DELETE action, optionally with an action-local path segment. */
69
- declare function deleteAction<const P extends string, const T extends RouteSchema>(path: P, schema: RouteDeclaration<T>): HttpAction<P, T, 'DELETE'>;
70
- declare function deleteAction<const T extends RouteSchema>(schema: RouteDeclaration<T>): HttpAction<'', T, 'DELETE'>;
68
+ declare function deleteAction<const P extends string, const T extends RouteSchema>(path: P, schema: T): HttpAction<P, T, 'DELETE'>;
69
+ declare function deleteAction<const T extends RouteSchema>(schema: T): HttpAction<'', T, 'DELETE'>;
71
70
  export { deleteAction as delete };
72
71
  /**
73
72
  * Declare a request body that is passed through to `fetch` without JSON encoding.
@@ -6,11 +6,8 @@ export type RouteMetadata = {
6
6
  /** Human-readable route description for generated tooling. */
7
7
  description?: string;
8
8
  };
9
- export type RouteMetadataMarker = {
10
- readonly [routeMetadataKey]: RouteMetadata;
11
- };
12
9
  /** Attach runtime metadata to a route declaration. */
13
- export declare function metadata(value: RouteMetadata): RouteMetadataMarker;
10
+ export declare function metadata(value: RouteMetadata): object;
14
11
  export declare function getRouteMetadata(value: unknown): RouteMetadata | undefined;
15
12
  export declare function stripRouteMetadata<T extends object>(value: T): Omit<T, typeof routeMetadataKey>;
16
13
  export {};
@@ -1,5 +1,4 @@
1
- import type { HattipHandler } from '@hattip/core';
2
- import { ApplyMiddleware, chain, ExtractMiddleware, MiddlewareChain, MiddlewareTypes } from 'alien-middleware';
1
+ import { ApplyMiddleware, chain, ExtractMiddleware, MiddlewareChain, MiddlewareTypes, RequestHandler } from 'alien-middleware';
3
2
  import { type HttpRouteTree } from '../http.js';
4
3
  import { type RouterResponsePlugin } from '../response.js';
5
4
  import type { RouteRequestHandlerMap } from '../types/server.js';
@@ -46,10 +45,10 @@ export type RouterConfig = {
46
45
  };
47
46
  };
48
47
  /**
49
- * Hattip-compatible Rouzer handler with chainable middleware and route
48
+ * Fetch-compatible Rouzer handler with chainable middleware and route
50
49
  * registration.
51
50
  */
52
- export interface Router<T extends MiddlewareTypes = any> extends HattipHandler<T['platform']>, MiddlewareChain<T> {
51
+ export interface Router<T extends MiddlewareTypes = any> extends RequestHandler<T>, MiddlewareChain<T> {
53
52
  /**
54
53
  * Clone this router and add the given middleware to the end of the chain.
55
54
  *
@@ -68,11 +67,11 @@ export interface Router<T extends MiddlewareTypes = any> extends HattipHandler<T
68
67
  use<TRoutes extends HttpRouteTree>(routes: TRoutes, handlers: RouteRequestHandlerMap<TRoutes, this>): Router<T>;
69
68
  }
70
69
  /**
71
- * Create a Rouzer router that can be mounted by any Hattip adapter.
70
+ * Create a Rouzer router that can be mounted as a fetch-compatible handler.
72
71
  *
73
72
  * @param config Optional router configuration for base path, debug behavior,
74
73
  * response plugins, and CORS origin restrictions.
75
- * @returns A Hattip-compatible handler with `.use(...)` methods for middleware
74
+ * @returns A fetch-compatible handler with `.use(...)` methods for middleware
76
75
  * and route registration.
77
76
  */
78
- export declare function createRouter<TEnv extends object = {}, TProperties extends object = {}, TPlatform = unknown>(config?: RouterConfig): Router<MiddlewareTypes<TEnv, TProperties, TPlatform>>;
77
+ export declare function createRouter<TEnv extends object = {}, TProperties extends object = {}, TRuntime = unknown>(config?: RouterConfig): Router<MiddlewareTypes<TEnv, TProperties, TRuntime>>;
@@ -142,11 +142,11 @@ class RouterObject extends MiddlewareChain {
142
142
  }
143
143
  }
144
144
  /**
145
- * Create a Rouzer router that can be mounted by any Hattip adapter.
145
+ * Create a Rouzer router that can be mounted as a fetch-compatible handler.
146
146
  *
147
147
  * @param config Optional router configuration for base path, debug behavior,
148
148
  * response plugins, and CORS origin restrictions.
149
- * @returns A Hattip-compatible handler with `.use(...)` methods for middleware
149
+ * @returns A fetch-compatible handler with `.use(...)` methods for middleware
150
150
  * and route registration.
151
151
  */
152
152
  export function createRouter(config = {}) {