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.
@@ -0,0 +1,136 @@
1
+ # Runtime and adapters
2
+
3
+ Rouzer routers are request handlers. Use adapter helpers to turn them into
4
+ functions accepted by your HTTP server, framework, or tests.
5
+
6
+ ## Plain Fetch Handler
7
+
8
+ For a plain Web `Request`, use the root `toFetchHandler` re-export.
9
+
10
+ ```ts
11
+ import { createRouter, toFetchHandler } from 'rouzer'
12
+
13
+ const router = createRouter().use(routes, handlers)
14
+ const fetchHandler = toFetchHandler(router)
15
+
16
+ const response = await fetchHandler(new Request('https://example.test/users'))
17
+ ```
18
+
19
+ `toFetchHandler(handler)` creates a Rouzer request context for each request and
20
+ calls the handler.
21
+
22
+ ## Host Data
23
+
24
+ Pass host data when middleware or handlers need environment variables, runtime
25
+ metadata, client IP, or background work support.
26
+
27
+ ```ts
28
+ const fetchHandler = toFetchHandler(router, {
29
+ host: request => ({
30
+ ip: request.headers.get('x-forwarded-for') ?? undefined,
31
+ runtime: { name: 'custom' },
32
+ env: name => process.env[name],
33
+ waitUntil: promise => {
34
+ void promise
35
+ },
36
+ }),
37
+ })
38
+ ```
39
+
40
+ Handlers read that data from the request context:
41
+
42
+ ```ts
43
+ ctx.host.ip
44
+ ctx.host.runtime?.name
45
+ ctx.env('DATABASE_URL')
46
+ ctx.waitUntil(writeAuditLog())
47
+ ```
48
+
49
+ Host runtime data lives under `ctx.host.runtime`.
50
+
51
+ ## Fetch-Compatible Servers
52
+
53
+ Many servers and frameworks accept a function that receives a Web `Request` and
54
+ returns a `Response`. Mount `toFetchHandler(router)` in those environments.
55
+
56
+ ```ts
57
+ const router = createRouter().use(routes, handlers)
58
+ const fetch = toFetchHandler(router)
59
+ ```
60
+
61
+ If the server exposes extra request metadata, pass it through the `host` option
62
+ so middleware and handlers can read it from `ctx.host`.
63
+
64
+ ## Custom Contexts
65
+
66
+ Use `createContext` when writing custom adapters or tests that call a handler
67
+ directly.
68
+
69
+ ```ts
70
+ import { createContext } from 'rouzer'
71
+
72
+ const context = createContext({
73
+ request: new Request('https://example.test/api/health'),
74
+ host: {
75
+ runtime: { name: 'test' },
76
+ env: name => process.env[name],
77
+ },
78
+ })
79
+
80
+ const response = await router(context)
81
+ ```
82
+
83
+ Most tests should prefer a local fetch wrapper because it exercises URL
84
+ construction and request creation through the client.
85
+
86
+ ```ts
87
+ import { toFetchHandler, type RequestHandler } from 'rouzer'
88
+
89
+ function createLocalFetch(handler: RequestHandler): typeof fetch {
90
+ const fetchHandler = toFetchHandler(handler)
91
+ return (input, init) => fetchHandler(new Request(input, init))
92
+ }
93
+ ```
94
+
95
+ ## CORS
96
+
97
+ Rouzer can restrict requests with an `Origin` header through router config.
98
+
99
+ ```ts
100
+ createRouter({
101
+ cors: {
102
+ allowOrigins: [
103
+ 'example.net',
104
+ 'https://*.example.com',
105
+ '*://localhost:3000',
106
+ ],
107
+ },
108
+ })
109
+ ```
110
+
111
+ Origins may contain wildcard protocol and subdomain segments. Origins without a
112
+ protocol default to `https`.
113
+
114
+ For allowed non-preflight requests, Rouzer sets
115
+ `Access-Control-Allow-Origin`. For preflight requests, Rouzer returns
116
+ `Access-Control-Allow-Origin`, `Access-Control-Allow-Methods`, and
117
+ `Access-Control-Allow-Headers`.
118
+
119
+ Rouzer does not set `Access-Control-Allow-Credentials`; set it yourself when
120
+ credentialed requests need it.
121
+
122
+ ## Background Work
123
+
124
+ Use `ctx.waitUntil(promise)` in middleware or handlers when the host supports
125
+ background work.
126
+
127
+ ```ts
128
+ ctx.waitUntil(
129
+ writeAuditLog({
130
+ route: ctx.url.pathname,
131
+ runtime: ctx.host.runtime?.name,
132
+ })
133
+ )
134
+ ```
135
+
136
+ The adapter delegates to `host.waitUntil` when you provide it.
@@ -0,0 +1,131 @@
1
+ # NDJSON streaming
2
+
3
+ Rouzer includes a response plugin for newline-delimited JSON response streams.
4
+ Use it when a route should send a sequence of JSON values without buffering the
5
+ whole response.
6
+
7
+ ```ts
8
+ import { createClient, createRouter } from 'rouzer'
9
+ import * as http from 'rouzer/http'
10
+ import * as ndjson from 'rouzer/ndjson'
11
+
12
+ type Event = {
13
+ id: number
14
+ message: string
15
+ }
16
+
17
+ export const events = http.get('events', {
18
+ response: ndjson.$type<Event>(),
19
+ })
20
+
21
+ export const routes = { events }
22
+
23
+ const router = createRouter({
24
+ plugins: [ndjson.routerPlugin],
25
+ }).use(routes, {
26
+ async *events() {
27
+ yield { id: 1, message: 'ready' }
28
+ yield { id: 2, message: 'done' }
29
+ },
30
+ })
31
+
32
+ const client = createClient({
33
+ baseURL: 'https://example.com/api/',
34
+ routes,
35
+ plugins: [ndjson.clientPlugin],
36
+ })
37
+
38
+ for await (const event of await client.events()) {
39
+ console.log(event.message)
40
+ }
41
+ ```
42
+
43
+ Handlers return an `Iterable<T>` or `AsyncIterable<T>`. The client action
44
+ resolves to an `AsyncIterable<T>`.
45
+
46
+ ## Plugin Registration
47
+
48
+ Register both sides when a route tree contains `ndjson.$type<T>()`:
49
+
50
+ - `ndjson.routerPlugin` in `createRouter({ plugins })`
51
+ - `ndjson.clientPlugin` in `createClient({ plugins })`
52
+
53
+ Rouzer fails fast if a route uses a response plugin marker and the matching
54
+ plugin is not registered.
55
+
56
+ ## POST Streams
57
+
58
+ NDJSON is a response codec. Requests still use normal Rouzer request schemas.
59
+ A route can receive a JSON body and return an NDJSON stream.
60
+
61
+ ```ts
62
+ export const streamEvents = http.post('events/stream', {
63
+ body: z.object({
64
+ topic: z.string(),
65
+ }),
66
+ response: ndjson.$type<Event>(),
67
+ })
68
+
69
+ createRouter({ plugins: [ndjson.routerPlugin] }).use(
70
+ { streamEvents },
71
+ {
72
+ async *streamEvents(ctx) {
73
+ yield { id: 1, message: `topic:${ctx.body.topic}` }
74
+ },
75
+ }
76
+ )
77
+ ```
78
+
79
+ Use `http.rawBody()` only when the request body itself should pass through as a
80
+ `BodyInit`.
81
+
82
+ ## Encoding And Decoding
83
+
84
+ `ndjson.routerPlugin` serializes each yielded value with `JSON.stringify` and
85
+ adds a newline. The response content type defaults to
86
+ `application/x-ndjson; charset=utf-8`.
87
+
88
+ `ndjson.clientPlugin` decodes UTF-8 chunks, accepts `\n` and `\r\n` line endings,
89
+ and parses each line with `JSON.parse`. A final line does not need a trailing
90
+ newline. Malformed JSON throws a `SyntaxError` with the line number.
91
+
92
+ Streamed items are not validated against a Zod schema. If item validation is
93
+ needed, validate before yielding on the server or while consuming on the client.
94
+
95
+ ## Cancellation
96
+
97
+ If a client aborts the request signal or stops iteration early by breaking from
98
+ `for await` or calling the iterator's `return()`, Rouzer cancels the response
99
+ body and calls the server source iterator's `return()`.
100
+
101
+ Sources that wait for future events should make those waits abort-aware when
102
+ cleanup needs to run while an awaited operation is still pending.
103
+
104
+ ```ts
105
+ async function readFirst<T>(source: AsyncIterable<T>) {
106
+ const iterator = source[Symbol.asyncIterator]()
107
+ try {
108
+ return (await iterator.next()).value
109
+ } finally {
110
+ await iterator.return?.()
111
+ }
112
+ }
113
+ ```
114
+
115
+ ## Stream Errors
116
+
117
+ Rouzer does not convert handler or generator failures into extra NDJSON items.
118
+ If an async generator throws after the response starts, the response stream
119
+ errors and the client's `for await` loop throws.
120
+
121
+ Model application-level stream errors as part of your item type when clients
122
+ should receive them as data.
123
+
124
+ ```ts
125
+ type StreamItem =
126
+ | { type: 'event'; id: number; message: string }
127
+ | { type: 'error'; message: string }
128
+ ```
129
+
130
+ The complete runnable version is
131
+ [examples/ndjson-stream.ts](../examples/ndjson-stream.ts).
@@ -1,6 +1,11 @@
1
- import type { HattipHandler } from '@hattip/core'
2
1
  import * as z from 'zod'
3
- import { $type, chain, createClient, createRouter } from 'rouzer'
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
- * Tiny Hattip adapter used only to keep this example self-contained. Real apps
35
- * mount the handler with a Hattip adapter for their runtime.
36
- */
37
- function createLocalFetch(handler: HattipHandler): typeof fetch {
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({ id: '42', includePosts: false }, { headers: { 'x-request-id': 'docs' } })
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 type { HattipHandler } from '@hattip/core'
2
- import { $error, $type, createClient, createRouter } from 'rouzer'
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
- * Tiny Hattip adapter used only to keep this example self-contained. Real apps
33
- * mount the handler with a Hattip adapter for their runtime.
34
- */
35
- function createLocalFetch(handler: HattipHandler): typeof fetch {
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 type { HattipHandler } from '@hattip/core'
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
- * Tiny Hattip adapter used only to keep this example self-contained. Real apps
36
- * mount the handler with a Hattip adapter for their runtime.
37
- */
38
- function createLocalFetch(handler: HattipHandler): typeof fetch {
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": "5.3.1",
3
+ "version": "6.0.0",
4
4
  "packageManager": "pnpm@11.5.1",
5
5
  "type": "module",
6
6
  "exports": {
@@ -28,7 +28,6 @@
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",
34
33
  "prettier": "^3.8.3",
@@ -40,9 +39,8 @@
40
39
  "zod": "^4.4.3"
41
40
  },
42
41
  "dependencies": {
43
- "@hattip/core": "^0.0.49",
44
42
  "@remix-run/route-pattern": "^0.22.1",
45
- "alien-middleware": "^0.11.6"
43
+ "alien-middleware": "^0.12.0"
46
44
  },
47
45
  "prettier": "@alloc/prettier-config",
48
46
  "license": "MIT",