rouzer 5.3.1 → 6.0.1
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 +215 -0
- package/docs/concepts.md +118 -0
- package/docs/config.json +18 -0
- package/docs/handlers.md +207 -0
- package/docs/index.md +103 -0
- package/docs/middleware.md +191 -0
- package/docs/migration-v5-to-v6.md +205 -0
- package/docs/patterns.md +147 -0
- package/docs/responses.md +191 -0
- package/docs/routes.md +206 -0
- package/docs/runtime.md +145 -0
- package/docs/streaming.md +139 -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 +3 -4
- package/docs/context.md +0 -623
package/docs/routes.md
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# Route contracts
|
|
2
|
+
|
|
3
|
+
> Define each HTTP operation once so its URL, validated input, handler type, and
|
|
4
|
+
> generated client call stay aligned.
|
|
5
|
+
|
|
6
|
+
Declare route contracts with the `rouzer/http` subpath. A route contract is the
|
|
7
|
+
shared source of truth for server handler types, client action types, URL
|
|
8
|
+
construction, and request validation.
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import { $type, metadata } from 'rouzer'
|
|
12
|
+
import * as http from 'rouzer/http'
|
|
13
|
+
import * as z from 'zod'
|
|
14
|
+
|
|
15
|
+
type Profile = {
|
|
16
|
+
id: string
|
|
17
|
+
name: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const profiles = http.resource('profiles/:id', {
|
|
21
|
+
...metadata({ description: 'Profile operations' }),
|
|
22
|
+
get: http.get({
|
|
23
|
+
query: z.object({
|
|
24
|
+
includePosts: z.optional(z.boolean()),
|
|
25
|
+
}),
|
|
26
|
+
response: $type<Profile>(),
|
|
27
|
+
}),
|
|
28
|
+
update: http.patch({
|
|
29
|
+
body: z.object({
|
|
30
|
+
name: z.string().check(z.minLength(1)),
|
|
31
|
+
}),
|
|
32
|
+
response: $type<Profile>(),
|
|
33
|
+
}),
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
export const routes = { profiles }
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Resources
|
|
40
|
+
|
|
41
|
+
Use `http.resource(path, children)` when actions share a path prefix or when the
|
|
42
|
+
client API should be namespaced.
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
export const organizations = http.resource('orgs/:orgId', {
|
|
46
|
+
members: http.resource('members/:memberId', {
|
|
47
|
+
get: http.get({ response: $type<Member>() }),
|
|
48
|
+
remove: http.delete({}),
|
|
49
|
+
}),
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
await client.organizations.members.get({
|
|
53
|
+
orgId: 'acme',
|
|
54
|
+
memberId: '42',
|
|
55
|
+
})
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Resource keys are API names. They do not affect the URL. Resource path patterns
|
|
59
|
+
and action-local path patterns are joined to produce the final route path.
|
|
60
|
+
|
|
61
|
+
## Actions
|
|
62
|
+
|
|
63
|
+
Use an action for each HTTP operation:
|
|
64
|
+
|
|
65
|
+
| Helper | Request schemas | Notes |
|
|
66
|
+
| ------------------ | -------------------------------------- | ------------------------------------------- |
|
|
67
|
+
| `http.get(...)` | `path`, `query`, `headers`, `response` | `GET` actions do not accept request bodies. |
|
|
68
|
+
| `http.post(...)` | `path`, `body`, `headers`, `response` | Mutation action. No `query` schema. |
|
|
69
|
+
| `http.put(...)` | `path`, `body`, `headers`, `response` | Mutation action. No `query` schema. |
|
|
70
|
+
| `http.patch(...)` | `path`, `body`, `headers`, `response` | Mutation action. No `query` schema. |
|
|
71
|
+
| `http.delete(...)` | `path`, `body`, `headers`, `response` | Mutation action. No `query` schema. |
|
|
72
|
+
|
|
73
|
+
Each helper accepts either `(schema)` or `(path, schema)`.
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
export const listProfiles = http.get('profiles', {
|
|
77
|
+
query: z.object({ page: z.optional(z.number()) }),
|
|
78
|
+
response: $type<Profile[]>(),
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
export const getProfile = http.get({
|
|
82
|
+
response: $type<Profile>(),
|
|
83
|
+
})
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The HTTP action API models explicit operations. It does not have an `ALL`
|
|
87
|
+
fallback route. Declare each supported method directly.
|
|
88
|
+
|
|
89
|
+
## Path Patterns
|
|
90
|
+
|
|
91
|
+
Rouzer uses `@remix-run/route-pattern` for path patterns. Common patterns
|
|
92
|
+
include:
|
|
93
|
+
|
|
94
|
+
- `profiles/:id`
|
|
95
|
+
- `v:major.:minor`
|
|
96
|
+
- `api(/v:major(.:minor))`
|
|
97
|
+
- `assets/*path`
|
|
98
|
+
- `search?q`
|
|
99
|
+
|
|
100
|
+
If you omit a `path` schema, TypeScript infers path params from the route
|
|
101
|
+
pattern and server handlers receive strings. Add a Zod `path` schema when you
|
|
102
|
+
need runtime validation, transforms, or non-string handler types.
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
export const profile = http.get('profiles/:id', {
|
|
106
|
+
path: z.object({
|
|
107
|
+
id: z.uuid(),
|
|
108
|
+
}),
|
|
109
|
+
response: $type<Profile>(),
|
|
110
|
+
})
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Full URL patterns can be used for top-level actions. Keep full URL patterns out
|
|
114
|
+
of resource/base-path composition because resources and router `basePath`
|
|
115
|
+
compose path segments.
|
|
116
|
+
|
|
117
|
+
## Request Schemas
|
|
118
|
+
|
|
119
|
+
Request schemas are Zod objects except for `body: http.rawBody()`.
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
export const updateProfile = http.patch('profiles/:id', {
|
|
123
|
+
path: z.object({ id: z.uuid() }),
|
|
124
|
+
body: z.object({ name: z.string() }),
|
|
125
|
+
headers: z.object({
|
|
126
|
+
'content-type': z.literal('application/json'),
|
|
127
|
+
}),
|
|
128
|
+
response: $type<Profile>(),
|
|
129
|
+
})
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
On the client, path, query, and JSON body fields are flattened into the first
|
|
133
|
+
action argument.
|
|
134
|
+
|
|
135
|
+
> [!IMPORTANT]
|
|
136
|
+
> Keep field names unique across path, query, and JSON body schemas. A flat
|
|
137
|
+
> client input cannot represent separate values for the same key.
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
await client.updateProfile({
|
|
141
|
+
id: 'a0f12d72-24e5-4eb0-bb70-6345f574e62a',
|
|
142
|
+
name: 'Ada',
|
|
143
|
+
})
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Per-request `RequestInit` options, including headers and abort signals, are
|
|
147
|
+
passed as the second argument.
|
|
148
|
+
|
|
149
|
+
## Raw Bodies
|
|
150
|
+
|
|
151
|
+
Use `http.rawBody()` when an action should pass a `BodyInit` through to `fetch`
|
|
152
|
+
without JSON encoding.
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
export const uploadAvatar = http.post('profiles/:id/avatar', {
|
|
156
|
+
body: http.rawBody(),
|
|
157
|
+
headers: z.object({ 'content-type': z.string() }),
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
await client.uploadAvatar(
|
|
161
|
+
{ id: '42' },
|
|
162
|
+
{ body: file, headers: { 'content-type': file.type } }
|
|
163
|
+
)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
> [!NOTE]
|
|
167
|
+
> Raw-body argument placement depends on route input. With path or query input,
|
|
168
|
+
> pass the body as `options.body`; without route input, pass the body as the
|
|
169
|
+
> first argument.
|
|
170
|
+
|
|
171
|
+
For a raw-body route without path or query input, the generated client accepts
|
|
172
|
+
the body as the first argument.
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
export const upload = http.post('uploads', {
|
|
176
|
+
body: http.rawBody(),
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
await client.upload(file, {
|
|
180
|
+
headers: { 'content-type': file.type },
|
|
181
|
+
})
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Server handlers for raw-body routes read from `ctx.request` with Fetch APIs such
|
|
185
|
+
as `arrayBuffer()`, `blob()`, `formData()`, or `text()`. Rouzer does not parse
|
|
186
|
+
or validate raw request bodies.
|
|
187
|
+
|
|
188
|
+
## Metadata
|
|
189
|
+
|
|
190
|
+
Use `metadata(...)` to attach optional runtime metadata to resources or actions.
|
|
191
|
+
Metadata does not affect routing, validation, client typing, or handler
|
|
192
|
+
behavior.
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
export const sessions = http.resource('sessions', {
|
|
196
|
+
...metadata({ description: 'Session control' }),
|
|
197
|
+
list: http.post('list', {
|
|
198
|
+
...metadata({ description: 'List sessions' }),
|
|
199
|
+
response: $type<SessionList>(),
|
|
200
|
+
}),
|
|
201
|
+
})
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Constructed route nodes expose metadata through `node.metadata`. Use it for
|
|
205
|
+
generated documentation, CLIs, route inspectors, or application-specific
|
|
206
|
+
tooling.
|
package/docs/runtime.md
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Runtime and adapters
|
|
2
|
+
|
|
3
|
+
> Adapt a Rouzer request handler to a Fetch runtime while keeping host-specific
|
|
4
|
+
> data behind the request context boundary.
|
|
5
|
+
|
|
6
|
+
Rouzer routers are request handlers. Use adapter helpers to turn them into
|
|
7
|
+
functions accepted by your HTTP server, framework, or tests.
|
|
8
|
+
|
|
9
|
+
## Plain Fetch Handler
|
|
10
|
+
|
|
11
|
+
For a plain Web `Request`, use the root `toFetchHandler` re-export.
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { createRouter, toFetchHandler } from 'rouzer'
|
|
15
|
+
|
|
16
|
+
const router = createRouter().use(routes, handlers)
|
|
17
|
+
const fetchHandler = toFetchHandler(router)
|
|
18
|
+
|
|
19
|
+
const response = await fetchHandler(new Request('https://example.test/users'))
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`toFetchHandler(handler)` creates a Rouzer request context for each request and
|
|
23
|
+
calls the handler.
|
|
24
|
+
|
|
25
|
+
## Host Data
|
|
26
|
+
|
|
27
|
+
Pass host data when middleware or handlers need environment variables, runtime
|
|
28
|
+
metadata, client IP, or background work support.
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
const fetchHandler = toFetchHandler(router, {
|
|
32
|
+
host: request => ({
|
|
33
|
+
ip: request.headers.get('x-forwarded-for') ?? undefined,
|
|
34
|
+
runtime: { name: 'custom' },
|
|
35
|
+
env: name => process.env[name],
|
|
36
|
+
waitUntil: promise => {
|
|
37
|
+
void promise
|
|
38
|
+
},
|
|
39
|
+
}),
|
|
40
|
+
})
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Handlers read that data from the request context:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
ctx.host.ip
|
|
47
|
+
ctx.host.runtime?.name
|
|
48
|
+
ctx.env('DATABASE_URL')
|
|
49
|
+
ctx.waitUntil(writeAuditLog())
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Host runtime data lives under `ctx.host.runtime`.
|
|
53
|
+
|
|
54
|
+
## Fetch-Compatible Servers
|
|
55
|
+
|
|
56
|
+
Many servers and frameworks accept a function that receives a Web `Request` and
|
|
57
|
+
returns a `Response`. Mount `toFetchHandler(router)` in those environments.
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
const router = createRouter().use(routes, handlers)
|
|
61
|
+
const fetch = toFetchHandler(router)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
If the server exposes extra request metadata, pass it through the `host` option
|
|
65
|
+
so middleware and handlers can read it from `ctx.host`.
|
|
66
|
+
|
|
67
|
+
## Custom Contexts
|
|
68
|
+
|
|
69
|
+
Use `createContext` when writing custom adapters or tests that call a handler
|
|
70
|
+
directly.
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
import { createContext } from 'rouzer'
|
|
74
|
+
|
|
75
|
+
const context = createContext({
|
|
76
|
+
request: new Request('https://example.test/api/health'),
|
|
77
|
+
host: {
|
|
78
|
+
runtime: { name: 'test' },
|
|
79
|
+
env: name => process.env[name],
|
|
80
|
+
},
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const response = await router(context)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Most tests should prefer a local fetch wrapper because it exercises URL
|
|
87
|
+
construction and request creation through the client.
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { toFetchHandler, type RequestHandler } from 'rouzer'
|
|
91
|
+
|
|
92
|
+
function createLocalFetch(handler: RequestHandler): typeof fetch {
|
|
93
|
+
const fetchHandler = toFetchHandler(handler)
|
|
94
|
+
return (input, init) => fetchHandler(new Request(input, init))
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## CORS
|
|
99
|
+
|
|
100
|
+
Rouzer can restrict requests with an `Origin` header through router config.
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
createRouter({
|
|
104
|
+
cors: {
|
|
105
|
+
allowOrigins: [
|
|
106
|
+
'example.net',
|
|
107
|
+
'https://*.example.com',
|
|
108
|
+
'*://localhost:3000',
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
})
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Origins may contain wildcard protocol and subdomain segments. Origins without a
|
|
115
|
+
protocol default to `https`.
|
|
116
|
+
|
|
117
|
+
For allowed non-preflight requests, Rouzer sets
|
|
118
|
+
`Access-Control-Allow-Origin`. For preflight requests, Rouzer returns
|
|
119
|
+
`Access-Control-Allow-Origin`, `Access-Control-Allow-Methods`, and
|
|
120
|
+
`Access-Control-Allow-Headers`.
|
|
121
|
+
|
|
122
|
+
> [!WARNING]
|
|
123
|
+
> Rouzer does not set `Access-Control-Allow-Credentials`. Set it yourself before
|
|
124
|
+
> relying on credentialed cross-origin requests.
|
|
125
|
+
|
|
126
|
+
## Background Work
|
|
127
|
+
|
|
128
|
+
Use `ctx.waitUntil(promise)` in middleware or handlers when the host supports
|
|
129
|
+
background work.
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
ctx.waitUntil(
|
|
133
|
+
writeAuditLog({
|
|
134
|
+
route: ctx.url.pathname,
|
|
135
|
+
runtime: ctx.host.runtime?.name,
|
|
136
|
+
})
|
|
137
|
+
)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The adapter delegates to `host.waitUntil` when you provide it.
|
|
141
|
+
|
|
142
|
+
> [!NOTE]
|
|
143
|
+
> Without `host.waitUntil`, the request context has no runtime-specific lifetime
|
|
144
|
+
> extension to delegate to. Provide it when background work must outlive the
|
|
145
|
+
> response.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# NDJSON streaming
|
|
2
|
+
|
|
3
|
+
> Stream a sequence of JSON values through a typed route while preserving
|
|
4
|
+
> incremental delivery, cancellation, and explicit error modeling.
|
|
5
|
+
|
|
6
|
+
Rouzer includes a response plugin for newline-delimited JSON response streams.
|
|
7
|
+
Use it when a route should send a sequence of JSON values without buffering the
|
|
8
|
+
whole response.
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import { createClient, createRouter } from 'rouzer'
|
|
12
|
+
import * as http from 'rouzer/http'
|
|
13
|
+
import * as ndjson from 'rouzer/ndjson'
|
|
14
|
+
|
|
15
|
+
type Event = {
|
|
16
|
+
id: number
|
|
17
|
+
message: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const events = http.get('events', {
|
|
21
|
+
response: ndjson.$type<Event>(),
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
export const routes = { events }
|
|
25
|
+
|
|
26
|
+
const router = createRouter({
|
|
27
|
+
plugins: [ndjson.routerPlugin],
|
|
28
|
+
}).use(routes, {
|
|
29
|
+
async *events() {
|
|
30
|
+
yield { id: 1, message: 'ready' }
|
|
31
|
+
yield { id: 2, message: 'done' }
|
|
32
|
+
},
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const client = createClient({
|
|
36
|
+
baseURL: 'https://example.com/api/',
|
|
37
|
+
routes,
|
|
38
|
+
plugins: [ndjson.clientPlugin],
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
for await (const event of await client.events()) {
|
|
42
|
+
console.log(event.message)
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Handlers return an `Iterable<T>` or `AsyncIterable<T>`. The client action
|
|
47
|
+
resolves to an `AsyncIterable<T>`.
|
|
48
|
+
|
|
49
|
+
## Plugin Registration
|
|
50
|
+
|
|
51
|
+
> [!IMPORTANT]
|
|
52
|
+
> Register both sides when a route tree contains `ndjson.$type<T>()`:
|
|
53
|
+
|
|
54
|
+
> - `ndjson.routerPlugin` in `createRouter({ plugins })`
|
|
55
|
+
> - `ndjson.clientPlugin` in `createClient({ plugins })`
|
|
56
|
+
|
|
57
|
+
Rouzer fails fast if a route uses a response plugin marker and the matching
|
|
58
|
+
plugin is not registered.
|
|
59
|
+
|
|
60
|
+
## POST Streams
|
|
61
|
+
|
|
62
|
+
NDJSON is a response codec. Requests still use normal Rouzer request schemas.
|
|
63
|
+
A route can receive a JSON body and return an NDJSON stream.
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
export const streamEvents = http.post('events/stream', {
|
|
67
|
+
body: z.object({
|
|
68
|
+
topic: z.string(),
|
|
69
|
+
}),
|
|
70
|
+
response: ndjson.$type<Event>(),
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
createRouter({ plugins: [ndjson.routerPlugin] }).use(
|
|
74
|
+
{ streamEvents },
|
|
75
|
+
{
|
|
76
|
+
async *streamEvents(ctx) {
|
|
77
|
+
yield { id: 1, message: `topic:${ctx.body.topic}` }
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Use `http.rawBody()` only when the request body itself should pass through as a
|
|
84
|
+
`BodyInit`.
|
|
85
|
+
|
|
86
|
+
## Encoding And Decoding
|
|
87
|
+
|
|
88
|
+
`ndjson.routerPlugin` serializes each yielded value with `JSON.stringify` and
|
|
89
|
+
adds a newline. The response content type defaults to
|
|
90
|
+
`application/x-ndjson; charset=utf-8`.
|
|
91
|
+
|
|
92
|
+
`ndjson.clientPlugin` decodes UTF-8 chunks, accepts `\n` and `\r\n` line endings,
|
|
93
|
+
and parses each line with `JSON.parse`. A final line does not need a trailing
|
|
94
|
+
newline. Malformed JSON throws a `SyntaxError` with the line number.
|
|
95
|
+
|
|
96
|
+
> [!NOTE]
|
|
97
|
+
> Streamed items are not validated against a Zod schema. If item validation is
|
|
98
|
+
> needed, validate before yielding on the server or while consuming on the
|
|
99
|
+
> client.
|
|
100
|
+
|
|
101
|
+
## Cancellation
|
|
102
|
+
|
|
103
|
+
If a client aborts the request signal or stops iteration early by breaking from
|
|
104
|
+
`for await` or calling the iterator's `return()`, Rouzer cancels the response
|
|
105
|
+
body and calls the server source iterator's `return()`.
|
|
106
|
+
|
|
107
|
+
> [!TIP]
|
|
108
|
+
> Make waits for future events abort-aware when cleanup must run while an
|
|
109
|
+
> awaited operation is still pending.
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
async function readFirst<T>(source: AsyncIterable<T>) {
|
|
113
|
+
const iterator = source[Symbol.asyncIterator]()
|
|
114
|
+
try {
|
|
115
|
+
return (await iterator.next()).value
|
|
116
|
+
} finally {
|
|
117
|
+
await iterator.return?.()
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Stream Errors
|
|
123
|
+
|
|
124
|
+
> [!NOTE]
|
|
125
|
+
> Rouzer does not convert handler or generator failures into extra NDJSON items.
|
|
126
|
+
> If an async generator throws after the response starts, the response stream
|
|
127
|
+
> errors and the client's `for await` loop throws.
|
|
128
|
+
|
|
129
|
+
Model application-level stream errors as part of your item type when clients
|
|
130
|
+
should receive them as data.
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
type StreamItem =
|
|
134
|
+
| { type: 'event'; id: number; message: string }
|
|
135
|
+
| { type: 'error'; message: string }
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The [complete runnable NDJSON example](https://github.com/alloc/rouzer/blob/main/examples/ndjson-stream.ts)
|
|
139
|
+
includes the shared route, router, client, and stream consumption loop.
|
package/examples/basic-usage.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import type { HattipHandler } from '@hattip/core'
|
|
2
1
|
import * as z from 'zod'
|
|
3
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
$type,
|
|
4
|
+
chain,
|
|
5
|
+
createClient,
|
|
6
|
+
createRouter,
|
|
7
|
+
toFetchHandler,
|
|
8
|
+
} from 'rouzer'
|
|
4
9
|
import * as http from 'rouzer/http'
|
|
5
10
|
|
|
6
11
|
type Profile = {
|
|
@@ -30,28 +35,11 @@ export const profiles = http.resource('profiles/:id', {
|
|
|
30
35
|
|
|
31
36
|
export const routes = { profiles }
|
|
32
37
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return async (input, init) => {
|
|
39
|
-
const request = new Request(input, init)
|
|
40
|
-
const response = await handler({
|
|
41
|
-
request,
|
|
42
|
-
ip: '127.0.0.1',
|
|
43
|
-
platform: undefined,
|
|
44
|
-
env() {
|
|
45
|
-
return undefined
|
|
46
|
-
},
|
|
47
|
-
passThrough() {},
|
|
48
|
-
waitUntil(promise) {
|
|
49
|
-
void promise
|
|
50
|
-
},
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
return response ?? new Response(null, { status: 404 })
|
|
54
|
-
}
|
|
38
|
+
function createLocalFetch(
|
|
39
|
+
handler: ReturnType<typeof createRouter>
|
|
40
|
+
): typeof fetch {
|
|
41
|
+
const fetchHandler = toFetchHandler(handler)
|
|
42
|
+
return (input, init) => fetchHandler(new Request(input, init))
|
|
55
43
|
}
|
|
56
44
|
|
|
57
45
|
export async function runBasicUsageExample() {
|
|
@@ -101,7 +89,10 @@ export async function runBasicUsageExample() {
|
|
|
101
89
|
fetch: createLocalFetch(handler),
|
|
102
90
|
})
|
|
103
91
|
|
|
104
|
-
const fetched = await client.profiles.get(
|
|
92
|
+
const fetched = await client.profiles.get(
|
|
93
|
+
{ id: '42', includePosts: false },
|
|
94
|
+
{ headers: { 'x-request-id': 'docs' } }
|
|
95
|
+
)
|
|
105
96
|
|
|
106
97
|
const updated = await client.profiles.update({ id: '42', name: 'Grace' })
|
|
107
98
|
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
$error,
|
|
3
|
+
$type,
|
|
4
|
+
createClient,
|
|
5
|
+
createRouter,
|
|
6
|
+
toFetchHandler,
|
|
7
|
+
} from 'rouzer'
|
|
3
8
|
import * as http from 'rouzer/http'
|
|
4
9
|
|
|
5
10
|
type User = {
|
|
@@ -28,28 +33,11 @@ export const getUser = http.get('users/:id', {
|
|
|
28
33
|
|
|
29
34
|
export const routes = { getUser }
|
|
30
35
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return async (input, init) => {
|
|
37
|
-
const request = new Request(input, init)
|
|
38
|
-
const response = await handler({
|
|
39
|
-
request,
|
|
40
|
-
ip: '127.0.0.1',
|
|
41
|
-
platform: undefined,
|
|
42
|
-
env() {
|
|
43
|
-
return undefined
|
|
44
|
-
},
|
|
45
|
-
passThrough() {},
|
|
46
|
-
waitUntil(promise) {
|
|
47
|
-
void promise
|
|
48
|
-
},
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
return response ?? new Response(null, { status: 404 })
|
|
52
|
-
}
|
|
36
|
+
function createLocalFetch(
|
|
37
|
+
handler: ReturnType<typeof createRouter>
|
|
38
|
+
): typeof fetch {
|
|
39
|
+
const fetchHandler = toFetchHandler(handler)
|
|
40
|
+
return (input, init) => fetchHandler(new Request(input, init))
|
|
53
41
|
}
|
|
54
42
|
|
|
55
43
|
export async function runErrorResponsesExample() {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { createClient, createRouter } from 'rouzer'
|
|
1
|
+
import { createClient, createRouter, toFetchHandler } from 'rouzer'
|
|
3
2
|
import * as http from 'rouzer/http'
|
|
4
3
|
import * as ndjson from 'rouzer/ndjson'
|
|
5
4
|
import { z } from 'zod'
|
|
@@ -31,28 +30,11 @@ export const stream = http.post('events/stream', {
|
|
|
31
30
|
|
|
32
31
|
export const routes = { events, stream }
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return async (input, init) => {
|
|
40
|
-
const request = new Request(input, init)
|
|
41
|
-
const response = await handler({
|
|
42
|
-
request,
|
|
43
|
-
ip: '127.0.0.1',
|
|
44
|
-
platform: undefined,
|
|
45
|
-
env() {
|
|
46
|
-
return undefined
|
|
47
|
-
},
|
|
48
|
-
passThrough() {},
|
|
49
|
-
waitUntil(promise) {
|
|
50
|
-
void promise
|
|
51
|
-
},
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
return response ?? new Response(null, { status: 404 })
|
|
55
|
-
}
|
|
33
|
+
function createLocalFetch(
|
|
34
|
+
handler: ReturnType<typeof createRouter>
|
|
35
|
+
): typeof fetch {
|
|
36
|
+
const fetchHandler = toFetchHandler(handler)
|
|
37
|
+
return (input, init) => fetchHandler(new Request(input, init))
|
|
56
38
|
}
|
|
57
39
|
|
|
58
40
|
async function collect<T>(source: AsyncIterable<T>) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rouzer",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.1",
|
|
4
4
|
"packageManager": "pnpm@11.5.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -28,9 +28,9 @@
|
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@alloc/prettier-config": "^1.0.0",
|
|
31
|
-
"@hattip/adapter-test": "^0.0.49",
|
|
32
31
|
"@types/node": "^25.8.0",
|
|
33
32
|
"@typescript/native-preview": "7.0.0-dev.20260515.1",
|
|
33
|
+
"lildocs": "^0.1.15",
|
|
34
34
|
"prettier": "^3.8.3",
|
|
35
35
|
"rouzer": "link:.",
|
|
36
36
|
"tsc-lint": "^0.1.9",
|
|
@@ -40,9 +40,8 @@
|
|
|
40
40
|
"zod": "^4.4.3"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@hattip/core": "^0.0.49",
|
|
44
43
|
"@remix-run/route-pattern": "^0.22.1",
|
|
45
|
-
"alien-middleware": "^0.
|
|
44
|
+
"alien-middleware": "^0.12.0"
|
|
46
45
|
},
|
|
47
46
|
"prettier": "@alloc/prettier-config",
|
|
48
47
|
"license": "MIT",
|