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/docs/context.md
DELETED
|
@@ -1,623 +0,0 @@
|
|
|
1
|
-
# Rouzer context
|
|
2
|
-
|
|
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 success,
|
|
6
|
-
error, or plugin response types.
|
|
7
|
-
|
|
8
|
-
## When to use Rouzer
|
|
9
|
-
|
|
10
|
-
Use Rouzer when:
|
|
11
|
-
|
|
12
|
-
- the same TypeScript project, package, or workspace can share route
|
|
13
|
-
declarations between server and client code
|
|
14
|
-
- request validation should run before server handlers and before client `fetch`
|
|
15
|
-
calls
|
|
16
|
-
- a Hattip-compatible handler fits your server runtime
|
|
17
|
-
- generated clients should stay close to route definitions instead of being
|
|
18
|
-
produced by a separate OpenAPI build step
|
|
19
|
-
|
|
20
|
-
Rouzer is not a server response validator, an OpenAPI generator, or a complete
|
|
21
|
-
server framework. It focuses on typed route contracts, request validation,
|
|
22
|
-
routing, and a small client wrapper. Response markers are type contracts; if
|
|
23
|
-
response data comes from an untrusted source, validate it where it enters your
|
|
24
|
-
server or client code instead of relying on the router to re-check handler
|
|
25
|
-
returns.
|
|
26
|
-
|
|
27
|
-
## Core abstractions
|
|
28
|
-
|
|
29
|
-
### HTTP route trees
|
|
30
|
-
|
|
31
|
-
Declare shared routes with the `rouzer/http` subpath:
|
|
32
|
-
|
|
33
|
-
```ts
|
|
34
|
-
import { $type } from 'rouzer'
|
|
35
|
-
import * as http from 'rouzer/http'
|
|
36
|
-
|
|
37
|
-
export const getProfile = http.get('profiles/:id', {
|
|
38
|
-
response: $type<Profile>(),
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
export const routes = { getProfile }
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
An action is a callable endpoint leaf. Use `http.get`, `http.post`, `http.put`,
|
|
45
|
-
`http.patch`, or `http.delete` to declare one HTTP operation. The key you put the
|
|
46
|
-
action under is the client and handler name; the action path is the URL pattern.
|
|
47
|
-
|
|
48
|
-
Use `http.resource(path, children)` when several actions share a path prefix or
|
|
49
|
-
when you want nested client/handler namespaces:
|
|
50
|
-
|
|
51
|
-
```ts
|
|
52
|
-
export const profiles = http.resource('profiles/:id', {
|
|
53
|
-
get: http.get({
|
|
54
|
-
response: $type<Profile>(),
|
|
55
|
-
}),
|
|
56
|
-
update: http.patch({
|
|
57
|
-
body: updateProfileSchema,
|
|
58
|
-
response: $type<Profile>(),
|
|
59
|
-
}),
|
|
60
|
-
posts: http.resource('posts', {
|
|
61
|
-
list: http.get({
|
|
62
|
-
response: $type<Post[]>(),
|
|
63
|
-
}),
|
|
64
|
-
}),
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
export const routes = { profiles }
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
Resource property names do not affect the URL. Resource paths and action-local
|
|
71
|
-
paths are joined, so the examples above expose `profiles/:id`, `profiles/:id`,
|
|
72
|
-
and `profiles/:id/posts`. Path params from parent resources are accumulated into
|
|
73
|
-
child action types.
|
|
74
|
-
|
|
75
|
-
Use the root `metadata(...)` helper to attach optional runtime metadata to
|
|
76
|
-
resources or actions without changing route matching, validation, client typing,
|
|
77
|
-
or handler behavior:
|
|
78
|
-
|
|
79
|
-
```ts
|
|
80
|
-
import { metadata } from 'rouzer'
|
|
81
|
-
|
|
82
|
-
export const sessions = http.resource('sessions', {
|
|
83
|
-
...metadata({
|
|
84
|
-
description: 'Daemon-managed session control.',
|
|
85
|
-
}),
|
|
86
|
-
list: http.post('list', {
|
|
87
|
-
...metadata({
|
|
88
|
-
description: 'Lists daemon-managed sessions and pagination state.',
|
|
89
|
-
}),
|
|
90
|
-
response: $type<SessionList>(),
|
|
91
|
-
}),
|
|
92
|
-
})
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
Constructed route nodes expose metadata through `node.metadata` for generated
|
|
96
|
-
clients, CLIs, docs, and route inspectors.
|
|
97
|
-
|
|
98
|
-
Patterns are parsed by `@remix-run/route-pattern` v0.21. Params can be inferred
|
|
99
|
-
from patterns such as `hello/:name`, `v:major.:minor`,
|
|
100
|
-
`api(/v:major(.:minor))`, `assets/*path`, and `search?q`. Full URL patterns such
|
|
101
|
-
as `https://:store.shopify.com/orders` are supported for top-level actions; keep
|
|
102
|
-
them out of resource/base-path composition.
|
|
103
|
-
|
|
104
|
-
### Method schemas
|
|
105
|
-
|
|
106
|
-
Method schemas describe the request pieces Rouzer should validate:
|
|
107
|
-
|
|
108
|
-
| Action helper | Request schemas | Notes |
|
|
109
|
-
| --------------------------------- | -------------------------------------- | ---------------- |
|
|
110
|
-
| `http.get(...)` | `path`, `query`, `headers`, `response` | No request body. |
|
|
111
|
-
| `http.post/put/patch/delete(...)` | `path`, `body`, `headers`, `response` | No query schema. `body` is a Zod object for JSON or `http.rawBody()` for pass-through payloads. |
|
|
112
|
-
|
|
113
|
-
If you omit a `path` schema, TypeScript infers path params from the pattern and
|
|
114
|
-
server handlers receive them as strings. Add a Zod `path` schema when you need
|
|
115
|
-
runtime validation, transforms, or non-string handler types.
|
|
116
|
-
|
|
117
|
-
The HTTP action API models explicit operations. It does not expose the old
|
|
118
|
-
method-map `ALL` fallback route shape; declare the concrete methods your client
|
|
119
|
-
and server support.
|
|
120
|
-
|
|
121
|
-
### Response markers and maps
|
|
122
|
-
|
|
123
|
-
`response: $type<T>()` is a TypeScript-only marker for JSON success payloads. It
|
|
124
|
-
tells handlers and client action functions what payload type to expect, but
|
|
125
|
-
Rouzer does not validate handler return values at the server boundary. Validate
|
|
126
|
-
response data where it enters your system, such as an external API client,
|
|
127
|
-
database decoder, or UI/client boundary, when runtime integrity is required.
|
|
128
|
-
|
|
129
|
-
Use a status-keyed response map when callers need to branch on declared statuses:
|
|
130
|
-
|
|
131
|
-
```ts
|
|
132
|
-
import { $error, $type } from 'rouzer'
|
|
133
|
-
import * as http from 'rouzer/http'
|
|
134
|
-
|
|
135
|
-
type User = { id: string; name: string }
|
|
136
|
-
type NotFound = { code: 'NOT_FOUND'; message: string }
|
|
137
|
-
|
|
138
|
-
export const getUser = http.get('users/:id', {
|
|
139
|
-
response: {
|
|
140
|
-
200: $type<User>(),
|
|
141
|
-
201: $type<User>(),
|
|
142
|
-
404: $error<NotFound>(),
|
|
143
|
-
},
|
|
144
|
-
})
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
Success entries use `$type<T>()` or a response plugin marker. Error entries use
|
|
148
|
-
`$error<T>()` and are encoded as JSON. Generated client action functions resolve
|
|
149
|
-
declared statuses as tuples:
|
|
150
|
-
|
|
151
|
-
- success: `[null, value, status]`
|
|
152
|
-
- error: `[error, null, status]`
|
|
153
|
-
|
|
154
|
-
Declared error statuses do not reject the client promise. Undeclared statuses
|
|
155
|
-
still go through `onJsonError` or throw the default error.
|
|
156
|
-
|
|
157
|
-
Handlers for response-map actions may return the default success value directly,
|
|
158
|
-
use `ctx.success(status, body)` to choose a declared success status, or use
|
|
159
|
-
`ctx.error(status, body)` to return a declared error status. The `ctx.error` and
|
|
160
|
-
`ctx.success` helpers only accept statuses and bodies declared in the response
|
|
161
|
-
map.
|
|
162
|
-
|
|
163
|
-
`response: ndjson.$type<T>()` is a TypeScript-only marker for newline-delimited
|
|
164
|
-
JSON response streams from the `rouzer/ndjson` subpath. Register
|
|
165
|
-
`ndjson.routerPlugin` with `createRouter(...)` and `ndjson.clientPlugin` with
|
|
166
|
-
`createClient(...)` for routes that use this marker. Handlers return an
|
|
167
|
-
`Iterable<T>` or `AsyncIterable<T>`; Rouzer serializes each item as one JSON line
|
|
168
|
-
and sets the response content type to `application/x-ndjson; charset=utf-8`.
|
|
169
|
-
Client action functions resolve to an `AsyncIterable<T>` parsed from the
|
|
170
|
-
response body. Streamed items are parsed as JSON but are not validated against a
|
|
171
|
-
Zod schema.
|
|
172
|
-
|
|
173
|
-
Actions without a `response` marker return a raw `Response` from client action
|
|
174
|
-
functions. Actions with `response: $type<T>()` return parsed JSON typed as `T`.
|
|
175
|
-
Actions with a response map return the tuple union described by that map.
|
|
176
|
-
|
|
177
|
-
### Response plugins
|
|
178
|
-
|
|
179
|
-
Response plugins add non-JSON response codecs without changing route matching or
|
|
180
|
-
request validation. A plugin package provides a compile-time response marker and
|
|
181
|
-
matching runtime plugins. For NDJSON, those are `ndjson.$type<T>()`,
|
|
182
|
-
`ndjson.routerPlugin`, and `ndjson.clientPlugin`.
|
|
183
|
-
|
|
184
|
-
The router plugin encodes non-`Response` handler results into an HTTP `Response`.
|
|
185
|
-
The client plugin decodes successful HTTP responses for generated client action
|
|
186
|
-
functions. Plugin markers can also be success entries in a status-keyed response
|
|
187
|
-
map. Rouzer validates plugin registration when routes are attached to a router or
|
|
188
|
-
client, so routes that use an unregistered response marker fail fast instead of
|
|
189
|
-
falling back to JSON. Response plugins do not automatically validate response
|
|
190
|
-
payloads unless the plugin itself implements validation.
|
|
191
|
-
|
|
192
|
-
### Router
|
|
193
|
-
|
|
194
|
-
`createRouter()` returns a Hattip-compatible handler. Use `.use(middleware)` to
|
|
195
|
-
append typed `alien-middleware` middleware and `.use(routes, handlers)` to attach
|
|
196
|
-
an HTTP route tree.
|
|
197
|
-
|
|
198
|
-
The handler object mirrors the route tree:
|
|
199
|
-
|
|
200
|
-
```ts
|
|
201
|
-
createRouter().use(routes, {
|
|
202
|
-
profiles: {
|
|
203
|
-
get(ctx) {
|
|
204
|
-
return loadProfile(ctx.path.id)
|
|
205
|
-
},
|
|
206
|
-
update(ctx) {
|
|
207
|
-
return updateProfile(ctx.path.id, ctx.body)
|
|
208
|
-
},
|
|
209
|
-
posts: {
|
|
210
|
-
list(ctx) {
|
|
211
|
-
return listPosts(ctx.path.id)
|
|
212
|
-
},
|
|
213
|
-
},
|
|
214
|
-
},
|
|
215
|
-
})
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
Handlers receive a context typed from middleware plus the action schema:
|
|
219
|
-
|
|
220
|
-
- `GET` handlers receive `ctx.path`, `ctx.query`, and `ctx.headers`
|
|
221
|
-
- mutation handlers receive `ctx.path`, `ctx.body`, and `ctx.headers`
|
|
222
|
-
- handlers may return a plain JSON-serializable value or a `Response`
|
|
223
|
-
- response-map handlers can return a default success value directly or use
|
|
224
|
-
`ctx.success(status, body)` and `ctx.error(status, body)`
|
|
225
|
-
- `ndjson.$type<T>()` handlers return an `Iterable<T>` or `AsyncIterable<T>`
|
|
226
|
-
unless they return a custom `Response`
|
|
227
|
-
- plain values are returned with `Response.json(value)`
|
|
228
|
-
- NDJSON iterables are returned as `application/x-ndjson` streams
|
|
229
|
-
- return a `Response` when you need custom status, headers, or body handling
|
|
230
|
-
|
|
231
|
-
`basePath` is prepended to route tree paths, `debug` adds matched-route debug
|
|
232
|
-
headers and more detailed validation errors, and `cors.allowOrigins` restricts
|
|
233
|
-
requests with an `Origin` header.
|
|
234
|
-
|
|
235
|
-
### Client
|
|
236
|
-
|
|
237
|
-
`createClient({ baseURL, routes })` creates a client tree that mirrors
|
|
238
|
-
`routes`, with action functions such as `client.profiles.get(args)`.
|
|
239
|
-
Generated action functions accept a flattened first argument containing path,
|
|
240
|
-
query, and JSON body fields. Per-request `RequestInit` options, including
|
|
241
|
-
headers and abort signals, are passed as the optional second argument. For
|
|
242
|
-
`http.rawBody()` routes, the raw `BodyInit` payload is passed through to `fetch`
|
|
243
|
-
without JSON encoding.
|
|
244
|
-
|
|
245
|
-
Generated action functions include:
|
|
246
|
-
|
|
247
|
-
- raw `Response` results for actions without a response schema
|
|
248
|
-
- parsed JSON and default non-2xx throwing for `$type<T>()` responses
|
|
249
|
-
- response-map support, returning `[error, value, status]` tuples for declared
|
|
250
|
-
statuses
|
|
251
|
-
- response plugin support, such as `ndjson.clientPlugin` for NDJSON response
|
|
252
|
-
streams
|
|
253
|
-
|
|
254
|
-
Prefer an absolute `baseURL` for generated client URLs:
|
|
255
|
-
|
|
256
|
-
```ts
|
|
257
|
-
const client = createClient({
|
|
258
|
-
baseURL: new URL('/api/', window.location.origin).href,
|
|
259
|
-
routes,
|
|
260
|
-
})
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
Default headers can be supplied with `headers`, per-request headers are merged on
|
|
264
|
-
top, and a custom `fetch` implementation can be supplied for tests or non-browser
|
|
265
|
-
runtimes. The returned client exposes the original options as `clientConfig`, so
|
|
266
|
-
route actions named `config` remain available as `client.config(...)`.
|
|
267
|
-
|
|
268
|
-
### Client lifecycle hooks
|
|
269
|
-
|
|
270
|
-
`createClient({ clientHook })` observes generated client action calls without
|
|
271
|
-
wrapping the returned client tree:
|
|
272
|
-
|
|
273
|
-
```ts
|
|
274
|
-
const client = createClient({
|
|
275
|
-
baseURL: 'https://example.com/api/',
|
|
276
|
-
routes,
|
|
277
|
-
clientHook(event) {
|
|
278
|
-
if (event.type === 'request.success') {
|
|
279
|
-
console.log({
|
|
280
|
-
opId: event.opId,
|
|
281
|
-
routeName: event.routeName,
|
|
282
|
-
durationMs: event.durationMs,
|
|
283
|
-
})
|
|
284
|
-
}
|
|
285
|
-
},
|
|
286
|
-
})
|
|
287
|
-
```
|
|
288
|
-
|
|
289
|
-
Rouzer emits:
|
|
290
|
-
|
|
291
|
-
- `request.start` before client-side validation
|
|
292
|
-
- `request.success` when the generated action resolves
|
|
293
|
-
- `request.error` when the generated action rejects
|
|
294
|
-
|
|
295
|
-
Each event includes an opaque per-call `opId`, the generated client route name
|
|
296
|
-
such as `session.create`, the HTTP method, the joined route path pattern, and
|
|
297
|
-
the generated action's first argument as `payload`. Terminal events include
|
|
298
|
-
`durationMs`, either the resolved `response` or thrown `error`, and `status` when
|
|
299
|
-
an HTTP response was received.
|
|
300
|
-
|
|
301
|
-
`request.error` covers client validation failures, transport failures,
|
|
302
|
-
undeclared HTTP errors, JSON parsing failures, response plugin decode failures,
|
|
303
|
-
and rejected `onJsonError` handlers. A declared error response returned as data
|
|
304
|
-
from a response map is a successful generated action and emits
|
|
305
|
-
`request.success`.
|
|
306
|
-
|
|
307
|
-
Lifecycle hooks are best-effort observability only. If `clientHook` throws,
|
|
308
|
-
Rouzer swallows that hook error and preserves the generated action's original
|
|
309
|
-
behavior.
|
|
310
|
-
|
|
311
|
-
For response plugins that return streams, such as NDJSON, `request.success` is
|
|
312
|
-
emitted when the generated action resolves to the stream object. Errors that
|
|
313
|
-
happen later while the caller consumes the stream are outside the first lifecycle
|
|
314
|
-
hook surface.
|
|
315
|
-
|
|
316
|
-
## Lifecycle
|
|
317
|
-
|
|
318
|
-
1. Define shared HTTP actions/resources with `rouzer/http` and Zod schemas.
|
|
319
|
-
2. Attach that route tree to a server with `createRouter().use(routes, handlers)`
|
|
320
|
-
or `createRouter({ plugins }).use(routes, handlers)` when response plugins
|
|
321
|
-
are needed.
|
|
322
|
-
3. Create a client with the same route tree, plus matching client response
|
|
323
|
-
plugins when needed.
|
|
324
|
-
4. Client action calls validate `path`, `query`, JSON object `body`, and
|
|
325
|
-
`headers` before `fetch`. Raw bodies are passed through without validation.
|
|
326
|
-
5. The router matches the request, validates the matched inputs, and calls the
|
|
327
|
-
handler.
|
|
328
|
-
6. Plain handler results become JSON responses, response-map helpers choose
|
|
329
|
-
declared statuses, plugin handler results become plugin-encoded responses, and
|
|
330
|
-
explicit `Response` objects pass through unchanged.
|
|
331
|
-
|
|
332
|
-
On the server, `path`, `query`, and `headers` values originate as strings. Rouzer
|
|
333
|
-
coerces Zod `number` schemas with `Number(value)` and Zod `boolean` schemas from
|
|
334
|
-
`"true"` and `"false"`. JSON request bodies are parsed and validated without that
|
|
335
|
-
string-coercion step. Raw request bodies declared with `http.rawBody()` are not
|
|
336
|
-
parsed by Rouzer.
|
|
337
|
-
|
|
338
|
-
## Common tasks
|
|
339
|
-
|
|
340
|
-
### Call client actions
|
|
341
|
-
|
|
342
|
-
Use generated client action functions for application calls. The first argument
|
|
343
|
-
is a flat object containing all path, query, and JSON body fields. The optional
|
|
344
|
-
second argument is for per-request `RequestInit` options such as headers or an
|
|
345
|
-
abort signal.
|
|
346
|
-
|
|
347
|
-
```ts
|
|
348
|
-
await client.profiles.get({ id: '42', includePosts: true })
|
|
349
|
-
await client.profiles.update(
|
|
350
|
-
{ id: '42', name: 'Ada' },
|
|
351
|
-
{ headers: { 'x-request-id': 'docs' } }
|
|
352
|
-
)
|
|
353
|
-
```
|
|
354
|
-
|
|
355
|
-
Avoid duplicate field names across an action's path, query, and body schemas;
|
|
356
|
-
the client input is flat, so duplicate keys cannot represent separate values.
|
|
357
|
-
|
|
358
|
-
### Handle declared error responses
|
|
359
|
-
|
|
360
|
-
Use `$error<T>()` inside a response map when an error status is part of the route
|
|
361
|
-
contract:
|
|
362
|
-
|
|
363
|
-
```ts
|
|
364
|
-
import { $error, $type, createClient, createRouter } from 'rouzer'
|
|
365
|
-
import * as http from 'rouzer/http'
|
|
366
|
-
|
|
367
|
-
type User = { id: string; name: string }
|
|
368
|
-
type NotFound = { code: 'NOT_FOUND'; message: string }
|
|
369
|
-
|
|
370
|
-
export const getUser = http.get('users/:id', {
|
|
371
|
-
response: {
|
|
372
|
-
200: $type<User>(),
|
|
373
|
-
404: $error<NotFound>(),
|
|
374
|
-
},
|
|
375
|
-
})
|
|
376
|
-
export const routes = { getUser }
|
|
377
|
-
|
|
378
|
-
createRouter().use(routes, {
|
|
379
|
-
getUser(ctx) {
|
|
380
|
-
if (ctx.path.id === 'missing') {
|
|
381
|
-
return ctx.error(404, {
|
|
382
|
-
code: 'NOT_FOUND',
|
|
383
|
-
message: 'User not found',
|
|
384
|
-
})
|
|
385
|
-
}
|
|
386
|
-
return { id: ctx.path.id, name: 'Ada' }
|
|
387
|
-
},
|
|
388
|
-
})
|
|
389
|
-
|
|
390
|
-
const client = createClient({
|
|
391
|
-
baseURL: 'https://example.com/api/',
|
|
392
|
-
routes,
|
|
393
|
-
})
|
|
394
|
-
|
|
395
|
-
const [error, user, status] = await client.getUser({ id: 'missing' })
|
|
396
|
-
|
|
397
|
-
if (status === 404) {
|
|
398
|
-
console.log(error.message)
|
|
399
|
-
} else {
|
|
400
|
-
console.log(user.name)
|
|
401
|
-
}
|
|
402
|
-
```
|
|
403
|
-
|
|
404
|
-
A complete runnable version lives in
|
|
405
|
-
[`examples/error-responses.ts`](../examples/error-responses.ts).
|
|
406
|
-
|
|
407
|
-
When a response map declares multiple success statuses, return a plain value for
|
|
408
|
-
the default success status or use `ctx.success(status, body)` to choose a
|
|
409
|
-
specific declared success status.
|
|
410
|
-
|
|
411
|
-
### Stream newline-delimited JSON
|
|
412
|
-
|
|
413
|
-
Use `ndjson.$type<T>()` when a handler should produce a sequence of JSON values
|
|
414
|
-
without buffering the whole response:
|
|
415
|
-
|
|
416
|
-
```ts
|
|
417
|
-
import { createClient, createRouter } from 'rouzer'
|
|
418
|
-
import * as http from 'rouzer/http'
|
|
419
|
-
import * as ndjson from 'rouzer/ndjson'
|
|
420
|
-
|
|
421
|
-
export const events = http.get('events', {
|
|
422
|
-
response: ndjson.$type<{ id: number; message: string }>(),
|
|
423
|
-
})
|
|
424
|
-
export const routes = { events }
|
|
425
|
-
|
|
426
|
-
createRouter({ plugins: [ndjson.routerPlugin] }).use(routes, {
|
|
427
|
-
async *events() {
|
|
428
|
-
yield { id: 1, message: 'ready' }
|
|
429
|
-
yield { id: 2, message: 'done' }
|
|
430
|
-
},
|
|
431
|
-
})
|
|
432
|
-
|
|
433
|
-
const client = createClient({
|
|
434
|
-
baseURL: 'https://example.com/api/',
|
|
435
|
-
routes,
|
|
436
|
-
plugins: [ndjson.clientPlugin],
|
|
437
|
-
})
|
|
438
|
-
for await (const event of await client.events()) {
|
|
439
|
-
console.log(event.message)
|
|
440
|
-
}
|
|
441
|
-
```
|
|
442
|
-
|
|
443
|
-
A complete runnable version lives in
|
|
444
|
-
[`examples/ndjson-stream.ts`](../examples/ndjson-stream.ts).
|
|
445
|
-
|
|
446
|
-
Rouzer's decoder accepts `\n` and `\r\n`, handles UTF-8 chunk boundaries, and
|
|
447
|
-
throws a `SyntaxError` with a line number for malformed JSON. If a consumer stops
|
|
448
|
-
reading early, the response body is cancelled.
|
|
449
|
-
|
|
450
|
-
If a client aborts the request signal or stops iteration early by breaking from
|
|
451
|
-
`for await` or calling the iterator's `return()`, Rouzer cancels the response
|
|
452
|
-
body and calls the server source iterator's `return()`. Sources that wait for
|
|
453
|
-
future events should make those waits abort-aware when they need cleanup to run
|
|
454
|
-
while an awaited operation is still pending.
|
|
455
|
-
|
|
456
|
-
Rouzer does not convert handler or generator failures into extra NDJSON items. If
|
|
457
|
-
an async generator throws after the response starts, the response stream errors
|
|
458
|
-
and the client's `for await` loop throws. Model application-level stream errors
|
|
459
|
-
as part of your item type, for example `{ type: 'error'; message: string }`, when
|
|
460
|
-
clients should receive them as data.
|
|
461
|
-
|
|
462
|
-
### Group resource actions
|
|
463
|
-
|
|
464
|
-
Use resources when the public API reads better as a tree or when actions share
|
|
465
|
-
path params:
|
|
466
|
-
|
|
467
|
-
```ts
|
|
468
|
-
export const organizations = http.resource('orgs/:orgId', {
|
|
469
|
-
members: http.resource('members/:memberId', {
|
|
470
|
-
get: http.get({ response: $type<Member>() }),
|
|
471
|
-
remove: http.delete({}),
|
|
472
|
-
}),
|
|
473
|
-
})
|
|
474
|
-
|
|
475
|
-
await client.organizations.members.get({ orgId: 'acme', memberId: '42' })
|
|
476
|
-
```
|
|
477
|
-
|
|
478
|
-
### Send raw request bodies
|
|
479
|
-
|
|
480
|
-
Use `http.rawBody()` for mutation actions whose client should pass a `BodyInit`
|
|
481
|
-
through to `fetch` without JSON encoding or Zod body parsing:
|
|
482
|
-
|
|
483
|
-
```ts
|
|
484
|
-
export const uploadAvatar = http.post('profiles/:id/avatar', {
|
|
485
|
-
body: http.rawBody(),
|
|
486
|
-
headers: z.object({ 'content-type': z.string() }),
|
|
487
|
-
})
|
|
488
|
-
|
|
489
|
-
await client.uploadAvatar(
|
|
490
|
-
{ id: '42' },
|
|
491
|
-
{ body: file, headers: { 'content-type': file.type } }
|
|
492
|
-
)
|
|
493
|
-
```
|
|
494
|
-
|
|
495
|
-
When a raw-body route has path or query input, path/query fields still live in
|
|
496
|
-
the flat first argument. The raw body itself is passed as `body` in the second
|
|
497
|
-
argument because it is a `RequestInit` value.
|
|
498
|
-
|
|
499
|
-
For raw-body routes without path or query input, the generated client action
|
|
500
|
-
accepts the body as the first argument and fetch options as the second:
|
|
501
|
-
|
|
502
|
-
```ts
|
|
503
|
-
export const upload = http.post('uploads', {
|
|
504
|
-
body: http.rawBody(),
|
|
505
|
-
})
|
|
506
|
-
|
|
507
|
-
await client.upload(file, {
|
|
508
|
-
headers: { 'content-type': file.type },
|
|
509
|
-
})
|
|
510
|
-
```
|
|
511
|
-
|
|
512
|
-
Server handlers for raw-body routes read from `ctx.request` directly with Fetch
|
|
513
|
-
APIs such as `arrayBuffer()`, `blob()`, `formData()`, or `text()`. Rouzer does
|
|
514
|
-
not parse or validate raw request bodies.
|
|
515
|
-
|
|
516
|
-
### Return custom responses
|
|
517
|
-
|
|
518
|
-
Return a `Response` from a handler for non-JSON payloads, custom status codes, or
|
|
519
|
-
custom headers. Return a plain value for the default `Response.json(value)` path.
|
|
520
|
-
|
|
521
|
-
### Customize JSON errors
|
|
522
|
-
|
|
523
|
-
By default, generated client action functions throw for
|
|
524
|
-
non-2xx responses that are not declared in a response map. If the response body
|
|
525
|
-
is JSON, its properties are copied onto the thrown `Error`.
|
|
526
|
-
|
|
527
|
-
`onJsonError` can override that behavior. Its return value is returned from the
|
|
528
|
-
response helper as-is; Rouzer does not automatically parse a returned `Response`
|
|
529
|
-
from `onJsonError`.
|
|
530
|
-
|
|
531
|
-
### v2->v3 migration
|
|
532
|
-
|
|
533
|
-
Rouzer now uses action/resource route trees for router registration and client
|
|
534
|
-
shorthands. In the v2->v3 migration, a method-map route such as this:
|
|
535
|
-
|
|
536
|
-
```ts
|
|
537
|
-
export const profileRoute = route('profiles/:id', {
|
|
538
|
-
GET: { response: $type<Profile>() },
|
|
539
|
-
PATCH: { body: updateProfileSchema, response: $type<Profile>() },
|
|
540
|
-
})
|
|
541
|
-
|
|
542
|
-
export const routes = { profileRoute }
|
|
543
|
-
```
|
|
544
|
-
|
|
545
|
-
becomes a named action tree:
|
|
546
|
-
|
|
547
|
-
```ts
|
|
548
|
-
import * as http from 'rouzer/http'
|
|
549
|
-
|
|
550
|
-
export const profiles = http.resource('profiles/:id', {
|
|
551
|
-
get: http.get({ response: $type<Profile>() }),
|
|
552
|
-
update: http.patch({
|
|
553
|
-
body: updateProfileSchema,
|
|
554
|
-
response: $type<Profile>(),
|
|
555
|
-
}),
|
|
556
|
-
})
|
|
557
|
-
|
|
558
|
-
export const routes = { profiles }
|
|
559
|
-
```
|
|
560
|
-
|
|
561
|
-
Handler maps mirror the action names, while v5 client calls use flat input
|
|
562
|
-
objects:
|
|
563
|
-
|
|
564
|
-
```ts
|
|
565
|
-
createRouter().use(routes, {
|
|
566
|
-
profiles: {
|
|
567
|
-
get(ctx) {
|
|
568
|
-
return loadProfile(ctx.path.id)
|
|
569
|
-
},
|
|
570
|
-
update(ctx) {
|
|
571
|
-
return updateProfile(ctx.path.id, ctx.body)
|
|
572
|
-
},
|
|
573
|
-
},
|
|
574
|
-
})
|
|
575
|
-
|
|
576
|
-
await client.profiles.get({ id: '42' })
|
|
577
|
-
await client.profiles.update({ id: '42', name: 'Ada' })
|
|
578
|
-
```
|
|
579
|
-
|
|
580
|
-
## Patterns to prefer
|
|
581
|
-
|
|
582
|
-
- Export route trees from a small shared module and import that module on both
|
|
583
|
-
server and client.
|
|
584
|
-
- Use `rouzer/http` actions for routes that are registered with
|
|
585
|
-
`createRouter().use(...)` or the required `createClient({ routes })` option.
|
|
586
|
-
- Add Zod schemas when you need runtime guarantees; rely on inferred path params
|
|
587
|
-
only when string params are sufficient.
|
|
588
|
-
- Use `response: $type<T>()` for JSON endpoints that should have typed client
|
|
589
|
-
action functions.
|
|
590
|
-
- Use response maps with `$error<T>()` when callers should handle declared error
|
|
591
|
-
statuses as typed data instead of exceptions.
|
|
592
|
-
- Use `response: ndjson.$type<T>()` plus `ndjson.routerPlugin` and
|
|
593
|
-
`ndjson.clientPlugin` for response streams where each line is a JSON value and
|
|
594
|
-
the client should consume an `AsyncIterable<T>`.
|
|
595
|
-
- Name actions after domain operations (`get`, `list`, `update`, `archive`) and
|
|
596
|
-
let `http.get/post/put/patch/delete` own the transport method.
|
|
597
|
-
- Set `content-type: application/json` yourself when your server or middleware
|
|
598
|
-
depends on that header.
|
|
599
|
-
|
|
600
|
-
## Constraints and gotchas
|
|
601
|
-
|
|
602
|
-
- `$type<T>()`, `$error<T>()`, and `ndjson.$type<T>()` are compile-time-only type
|
|
603
|
-
contracts. Rouzer does not re-validate handler return values at the server
|
|
604
|
-
boundary.
|
|
605
|
-
- NDJSON support is for response streams; request bodies use JSON body schemas
|
|
606
|
-
unless an action declares `body: http.rawBody()`.
|
|
607
|
-
- Declared `$error<T>()` responses are JSON responses. Use a custom `Response`
|
|
608
|
-
for non-JSON error payloads.
|
|
609
|
-
- Routes that use a response plugin fail fast if the matching client or router
|
|
610
|
-
plugin is not registered.
|
|
611
|
-
- Pathname route patterns expect an absolute client `baseURL`.
|
|
612
|
-
- Resource and action keys are API names only; paths come from the pattern
|
|
613
|
-
strings passed to `http.resource(...)` and action helpers.
|
|
614
|
-
- Path, query, and JSON body fields are flattened into the first client action
|
|
615
|
-
argument. Per-request `RequestInit` fields, such as `signal`, `credentials`,
|
|
616
|
-
and `headers`, belong in the second argument. `method` is reserved by Rouzer.
|
|
617
|
-
For `http.rawBody()` actions, `body` is accepted in the second argument when
|
|
618
|
-
the route has path or query input; raw-body actions without route input accept
|
|
619
|
-
the body as the first argument.
|
|
620
|
-
- The HTTP action API has no `ALL` fallback route. Declare explicit actions for
|
|
621
|
-
supported methods.
|
|
622
|
-
- Rouzer does not automatically set `Access-Control-Allow-Credentials`; set it in
|
|
623
|
-
your handler when credentialed cross-origin requests need it.
|