rouzer 5.3.1 → 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 +113 -231
- package/dist/server/router.d.ts +6 -7
- package/dist/server/router.js +2 -2
- package/docs/client.md +203 -0
- package/docs/concepts.md +113 -0
- package/docs/handlers.md +202 -0
- package/docs/index.md +78 -0
- package/docs/middleware.md +185 -0
- package/docs/migration-v5-to-v6.md +200 -0
- package/docs/patterns.md +139 -0
- package/docs/responses.md +183 -0
- package/docs/routes.md +195 -0
- package/docs/runtime.md +136 -0
- package/docs/streaming.md +131 -0
- package/examples/basic-usage.ts +16 -25
- package/examples/error-responses.ts +12 -24
- package/examples/ndjson-stream.ts +6 -24
- package/package.json +2 -4
- package/docs/context.md +0 -623
package/README.md
CHANGED
|
@@ -1,278 +1,160 @@
|
|
|
1
1
|
# Rouzer
|
|
2
2
|
|
|
3
|
-
Rouzer
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10
|
-
optional JSON, error, or newline-delimited JSON response types once, then reuses
|
|
11
|
-
that contract to:
|
|
12
|
+
## Use It When
|
|
12
13
|
|
|
13
|
-
|
|
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
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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 {
|
|
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`
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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<
|
|
75
|
+
response: $type<Profile>(),
|
|
201
76
|
}),
|
|
202
77
|
})
|
|
203
|
-
```
|
|
204
78
|
|
|
205
|
-
|
|
79
|
+
export const routes = { profiles }
|
|
206
80
|
|
|
207
|
-
|
|
81
|
+
const requestInfo = chain().use(ctx => ({
|
|
82
|
+
requestId: ctx.request.headers.get('x-request-id') ?? 'local',
|
|
83
|
+
}))
|
|
208
84
|
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
106
|
+
const profile = await client.profiles.get(
|
|
107
|
+
{ id: '42', includePosts: false },
|
|
108
|
+
{ headers: { 'x-request-id': 'docs' } }
|
|
109
|
+
)
|
|
110
|
+
```
|
|
228
111
|
|
|
229
|
-
|
|
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
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
243
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
264
|
-
`
|
|
265
|
-
|
|
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
|
-
- [
|
|
272
|
-
- [
|
|
273
|
-
- [
|
|
274
|
-
- [
|
|
275
|
-
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
-
|
|
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/server/router.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import
|
|
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
|
-
*
|
|
48
|
+
* Fetch-compatible Rouzer handler with chainable middleware and route
|
|
50
49
|
* registration.
|
|
51
50
|
*/
|
|
52
|
-
export interface Router<T extends MiddlewareTypes = any> extends
|
|
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
|
|
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
|
|
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 = {},
|
|
77
|
+
export declare function createRouter<TEnv extends object = {}, TProperties extends object = {}, TRuntime = unknown>(config?: RouterConfig): Router<MiddlewareTypes<TEnv, TProperties, TRuntime>>;
|
package/dist/server/router.js
CHANGED
|
@@ -142,11 +142,11 @@ class RouterObject extends MiddlewareChain {
|
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
/**
|
|
145
|
-
* Create a Rouzer router that can be mounted
|
|
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
|
|
149
|
+
* @returns A fetch-compatible handler with `.use(...)` methods for middleware
|
|
150
150
|
* and route registration.
|
|
151
151
|
*/
|
|
152
152
|
export function createRouter(config = {}) {
|
package/docs/client.md
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# Typed client
|
|
2
|
+
|
|
3
|
+
`createClient({ baseURL, routes })` creates a typed fetch client from the same
|
|
4
|
+
route tree used by the server.
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
const client = createClient({
|
|
8
|
+
baseURL: 'https://example.com/api/',
|
|
9
|
+
routes,
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
const profile = await client.profiles.get({
|
|
13
|
+
id: '42',
|
|
14
|
+
includePosts: true,
|
|
15
|
+
})
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The returned client mirrors resources and actions from the route tree. Each
|
|
19
|
+
action function validates input before calling `fetch`.
|
|
20
|
+
|
|
21
|
+
## Configuration
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
const client = createClient({
|
|
25
|
+
baseURL: new URL('/api/', window.location.origin).href,
|
|
26
|
+
routes,
|
|
27
|
+
headers: {
|
|
28
|
+
'content-type': 'application/json',
|
|
29
|
+
},
|
|
30
|
+
fetch: globalThis.fetch,
|
|
31
|
+
plugins: [ndjson.clientPlugin],
|
|
32
|
+
onJsonError(response) {
|
|
33
|
+
return response.json()
|
|
34
|
+
},
|
|
35
|
+
clientHook(event) {
|
|
36
|
+
console.log(event.type, event.routeName)
|
|
37
|
+
},
|
|
38
|
+
})
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
| Option | Purpose |
|
|
42
|
+
| ------------- | ------------------------------------------------------------------------------------- |
|
|
43
|
+
| `baseURL` | Absolute base URL used to build request URLs. A trailing slash is added when missing. |
|
|
44
|
+
| `routes` | Shared HTTP route tree. Required. |
|
|
45
|
+
| `headers` | Default headers merged into every request. |
|
|
46
|
+
| `fetch` | Custom fetch implementation for tests or non-browser runtimes. |
|
|
47
|
+
| `plugins` | Client response plugins such as `ndjson.clientPlugin`. |
|
|
48
|
+
| `onJsonError` | Custom handler for non-2xx responses not declared in a response map. |
|
|
49
|
+
| `clientHook` | Best-effort lifecycle observer for generated action calls. |
|
|
50
|
+
|
|
51
|
+
The returned client exposes the original options as `client.clientConfig`, so a
|
|
52
|
+
route action named `config` remains available as `client.config(...)`.
|
|
53
|
+
|
|
54
|
+
## Action Arguments
|
|
55
|
+
|
|
56
|
+
Generated action functions use a flat first argument for path, query, and JSON
|
|
57
|
+
body fields.
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
export const updateProfile = http.patch('profiles/:id', {
|
|
61
|
+
body: z.object({
|
|
62
|
+
name: z.string(),
|
|
63
|
+
}),
|
|
64
|
+
response: $type<Profile>(),
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
await client.updateProfile({
|
|
68
|
+
id: '42',
|
|
69
|
+
name: 'Ada',
|
|
70
|
+
})
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Per-request `RequestInit` options are the second argument. Rouzer reserves
|
|
74
|
+
`method` and owns JSON body encoding. Headers are typed from the route header
|
|
75
|
+
schema when one exists.
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
await client.profiles.get(
|
|
79
|
+
{ id: '42', includePosts: false },
|
|
80
|
+
{
|
|
81
|
+
signal: abortController.signal,
|
|
82
|
+
headers: { 'x-request-id': 'docs' },
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Avoid duplicate keys across path, query, and JSON body schemas. The flat client
|
|
88
|
+
input cannot distinguish duplicate field names from different request
|
|
89
|
+
locations.
|
|
90
|
+
|
|
91
|
+
## Raw Body Routes
|
|
92
|
+
|
|
93
|
+
For raw-body routes with path or query input, pass the `BodyInit` as
|
|
94
|
+
`options.body`.
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
await client.uploadAvatar(
|
|
98
|
+
{ id: '42' },
|
|
99
|
+
{ body: file, headers: { 'content-type': file.type } }
|
|
100
|
+
)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
For raw-body routes without route input, pass the body as the first argument.
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
await client.upload(file, {
|
|
107
|
+
headers: { 'content-type': file.type },
|
|
108
|
+
})
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Rouzer does not JSON encode or validate raw bodies.
|
|
112
|
+
|
|
113
|
+
## Response Results
|
|
114
|
+
|
|
115
|
+
Generated action return types depend on the action response schema:
|
|
116
|
+
|
|
117
|
+
| Route response | Client result |
|
|
118
|
+
| ------------------------- | ---------------------------------------------------------------- |
|
|
119
|
+
| No `response` marker | Raw `Response`. |
|
|
120
|
+
| `response: $type<T>()` | Parsed JSON typed as `T`. |
|
|
121
|
+
| Status-keyed response map | Tuple union: `[null, value, status]` or `[error, null, status]`. |
|
|
122
|
+
| Response plugin marker | Plugin-decoded value, such as `AsyncIterable<T>` for NDJSON. |
|
|
123
|
+
|
|
124
|
+
Non-2xx responses reject unless the status is declared in a response map or
|
|
125
|
+
`onJsonError` returns a value.
|
|
126
|
+
|
|
127
|
+
## Error Handling
|
|
128
|
+
|
|
129
|
+
By default, undeclared non-2xx responses throw an `Error` with the HTTP status
|
|
130
|
+
in the message. If the response has a JSON content type, Rouzer copies parsed
|
|
131
|
+
JSON properties onto the thrown error.
|
|
132
|
+
|
|
133
|
+
Use `onJsonError` to override that behavior:
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
const client = createClient({
|
|
137
|
+
baseURL,
|
|
138
|
+
routes,
|
|
139
|
+
async onJsonError(response) {
|
|
140
|
+
return {
|
|
141
|
+
status: response.status,
|
|
142
|
+
body: await response.json(),
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
})
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Rouzer returns the `onJsonError` result as-is. It does not automatically parse a
|
|
149
|
+
`Response` returned by `onJsonError`.
|
|
150
|
+
|
|
151
|
+
## Lifecycle Hooks
|
|
152
|
+
|
|
153
|
+
Use `clientHook` for observability without wrapping every generated action.
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
const client = createClient({
|
|
157
|
+
baseURL,
|
|
158
|
+
routes,
|
|
159
|
+
clientHook(event) {
|
|
160
|
+
if (event.type === 'request.success') {
|
|
161
|
+
console.log(event.routeName, event.durationMs)
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
})
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Rouzer emits:
|
|
168
|
+
|
|
169
|
+
- `request.start` before client-side validation
|
|
170
|
+
- `request.success` when the generated action resolves
|
|
171
|
+
- `request.error` when the generated action rejects
|
|
172
|
+
|
|
173
|
+
Each event includes an opaque `opId`, `routeName`, HTTP `method`,
|
|
174
|
+
`pathPattern`, and original `payload`. Terminal events include `durationMs` and
|
|
175
|
+
either `response` or `error`. If an HTTP response was received, terminal events
|
|
176
|
+
also include `status`.
|
|
177
|
+
|
|
178
|
+
Hook errors are swallowed. Lifecycle hooks are observability-only and must not
|
|
179
|
+
change request behavior.
|
|
180
|
+
|
|
181
|
+
For streaming response plugins, `request.success` is emitted when the generated
|
|
182
|
+
action resolves to the stream object. Errors that happen while consuming the
|
|
183
|
+
stream are outside the first lifecycle hook surface.
|
|
184
|
+
|
|
185
|
+
## Testing With Local Fetch
|
|
186
|
+
|
|
187
|
+
Rouzer handlers accept a request context, while `fetch` accepts `(input, init)`.
|
|
188
|
+
Use `toFetchHandler` plus a small wrapper in tests.
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
import { toFetchHandler, type RequestHandler } from 'rouzer'
|
|
192
|
+
|
|
193
|
+
function createLocalFetch(handler: RequestHandler): typeof fetch {
|
|
194
|
+
const fetchHandler = toFetchHandler(handler)
|
|
195
|
+
return (input, init) => fetchHandler(new Request(input, init))
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const client = createClient({
|
|
199
|
+
baseURL: 'https://example.test/api/',
|
|
200
|
+
routes,
|
|
201
|
+
fetch: createLocalFetch(router),
|
|
202
|
+
})
|
|
203
|
+
```
|