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,113 @@
1
+ # Framework concepts
2
+
3
+ Rouzer is for applications that can share a TypeScript HTTP contract between
4
+ server and client code. The central object is an HTTP route tree. That tree
5
+ describes URL paths, action names, request schemas, and response contracts once,
6
+ then it is reused by the router and generated client.
7
+
8
+ Rouzer is not an OpenAPI generator, a response validator, or a full application
9
+ framework. It focuses on shared route contracts, request validation, typed
10
+ handlers, typed clients, response helpers, middleware, and request context.
11
+
12
+ ## The Main Objects
13
+
14
+ Route tree:
15
+
16
+ - a plain object whose leaves are HTTP actions and whose branches are resources
17
+ - exported from a shared module that server and client code can both import
18
+ - passed to `createRouter().use(routes, handlers)` and
19
+ `createClient({ routes, baseURL })`
20
+
21
+ Action:
22
+
23
+ - a concrete HTTP operation declared with `http.get`, `http.post`, `http.put`,
24
+ `http.patch`, or `http.delete`
25
+ - owns an optional action-local path, request schemas, and an optional response
26
+ marker or response map
27
+ - becomes one handler function on the server and one generated client function
28
+
29
+ Resource:
30
+
31
+ - a path-scoped namespace declared with `http.resource(path, children)`
32
+ - contributes path params to child actions
33
+ - creates nested handler and client objects
34
+
35
+ Router:
36
+
37
+ - a fetch-compatible request handler returned by `createRouter(...)`
38
+ - accepts middleware with `.use(middleware)`
39
+ - accepts route trees with `.use(routes, handlers)`
40
+ - validates matched requests before handlers run
41
+
42
+ Client:
43
+
44
+ - a typed fetch wrapper returned by `createClient({ baseURL, routes })`
45
+ - mirrors the route tree shape
46
+ - validates client input before sending requests
47
+ - parses responses according to the route response contract
48
+
49
+ Middleware:
50
+
51
+ - functions that receive a shared `RequestContext`
52
+ - can add typed properties, environment bindings, response callbacks, or runtime
53
+ type markers
54
+ - run before route handlers when attached before `.use(routes, handlers)`
55
+
56
+ ## What Validation Covers
57
+
58
+ Rouzer validates request inputs:
59
+
60
+ - path params from route patterns or a `path` Zod object
61
+ - URL query values on `GET` actions with a `query` schema
62
+ - JSON request bodies on mutation actions with a Zod `body` schema
63
+ - request headers with a `headers` schema
64
+
65
+ Path, query, and header values arrive as strings. Rouzer adds string parsing for
66
+ Zod number and boolean schemas in those locations, including nested object and
67
+ array schemas. JSON request bodies are parsed from the request body and then
68
+ validated as JSON values.
69
+
70
+ Rouzer does not validate handler return values just because a route uses
71
+ `$type<T>()`, `$error<T>()`, or `ndjson.$type<T>()`. Those markers are TypeScript
72
+ contracts. Validate untrusted response data where it enters your system.
73
+
74
+ ## Lifecycle
75
+
76
+ ```ts
77
+ // shared/routes.ts
78
+ export const routes = {
79
+ profiles: http.resource('profiles/:id', {
80
+ get: http.get({ response: $type<Profile>() }),
81
+ }),
82
+ }
83
+
84
+ // server.ts
85
+ export const router = createRouter()
86
+ .use(requestMiddleware)
87
+ .use(routes, handlers)
88
+
89
+ // client.ts
90
+ export const client = createClient({
91
+ baseURL: 'https://example.com/api/',
92
+ routes,
93
+ })
94
+ ```
95
+
96
+ The same action key becomes:
97
+
98
+ - `handlers.profiles.get(ctx)` on the server
99
+ - `client.profiles.get(input, options)` on the client
100
+ - a route name such as `profiles.get` in client lifecycle hook events
101
+
102
+ ## Rouzer Middleware
103
+
104
+ Rouzer's router is both callable as a request handler and chainable with
105
+ middleware helpers.
106
+
107
+ Use route contracts for HTTP handler/client behavior. Use middleware when you
108
+ need request-scoped state, authentication, environment bindings, host runtime
109
+ data, response callbacks, background work, or custom adapter contexts.
110
+
111
+ The common middleware API is exported from `rouzer`, including `chain`,
112
+ `toFetchHandler`, `createContext`, `filterRuntime`, `RequestContext`, and
113
+ `RequestHandler`.
@@ -0,0 +1,202 @@
1
+ # Routers and handlers
2
+
3
+ `createRouter()` returns a fetch-compatible Rouzer handler with chain methods.
4
+ Use it to compose middleware and attach route trees.
5
+
6
+ ```ts
7
+ import { createRouter } from 'rouzer'
8
+
9
+ export const router = createRouter({ basePath: 'api/' })
10
+ .use(requestMiddleware)
11
+ .use(routes, handlers)
12
+ ```
13
+
14
+ The router is also a `RequestHandler`, so it can be mounted with
15
+ `toFetchHandler(router)` or runtime-specific adapter helpers.
16
+
17
+ ## Handler Maps
18
+
19
+ The handler object mirrors the route tree. Resource nodes become nested objects.
20
+ Action nodes become handler functions.
21
+
22
+ ```ts
23
+ export const routes = {
24
+ profiles: http.resource('profiles/:id', {
25
+ get: http.get({ response: $type<Profile>() }),
26
+ update: http.patch({
27
+ body: updateProfileSchema,
28
+ response: $type<Profile>(),
29
+ }),
30
+ posts: http.resource('posts', {
31
+ list: http.get({ response: $type<Post[]>() }),
32
+ }),
33
+ }),
34
+ }
35
+
36
+ createRouter().use(routes, {
37
+ profiles: {
38
+ get(ctx) {
39
+ return loadProfile(ctx.path.id)
40
+ },
41
+ update(ctx) {
42
+ return updateProfile(ctx.path.id, ctx.body)
43
+ },
44
+ posts: {
45
+ list(ctx) {
46
+ return listPosts(ctx.path.id)
47
+ },
48
+ },
49
+ },
50
+ })
51
+ ```
52
+
53
+ ## Handler Context
54
+
55
+ Handlers receive the Rouzer request context plus values inferred from the
56
+ matched route.
57
+
58
+ `GET` handlers receive:
59
+
60
+ - `ctx.path`
61
+ - `ctx.query`
62
+ - `ctx.headers`
63
+
64
+ Mutation handlers receive:
65
+
66
+ - `ctx.path`
67
+ - `ctx.body`
68
+ - `ctx.headers`
69
+
70
+ All handlers also receive middleware-provided properties and base context fields
71
+ such as `ctx.request`, `ctx.url`, `ctx.host`, `ctx.env(...)`,
72
+ `ctx.waitUntil(...)`, `ctx.setHeader(...)`, and `ctx.onResponse(...)`.
73
+
74
+ ```ts
75
+ const requestInfo = chain().use(ctx => ({
76
+ requestId: ctx.request.headers.get('x-request-id') ?? 'local',
77
+ }))
78
+
79
+ createRouter()
80
+ .use(requestInfo)
81
+ .use(routes, {
82
+ getProfile(ctx) {
83
+ return {
84
+ id: ctx.path.id,
85
+ requestId: ctx.requestId,
86
+ }
87
+ },
88
+ })
89
+ ```
90
+
91
+ ## Validation Order
92
+
93
+ For a matched route, Rouzer validates before the handler runs:
94
+
95
+ 1. path params, if a `path` schema is declared
96
+ 2. headers, if a `headers` schema is declared
97
+ 3. query string, for `GET` actions with a `query` schema
98
+ 4. JSON body, for mutation actions with a non-raw `body` schema
99
+
100
+ If validation fails, Rouzer returns a `400` JSON response and does not call the
101
+ handler. In `debug` mode, validation responses include more specific Zod error
102
+ messages.
103
+
104
+ If no `path` schema is declared, `ctx.path` is inferred from the route pattern
105
+ and contains string params.
106
+
107
+ ## Return Values
108
+
109
+ Handlers can return:
110
+
111
+ - a plain value, which Rouzer sends with `Response.json(value)`
112
+ - a Web `Response`, which Rouzer passes through unchanged
113
+ - `ctx.error(status, body)` for a declared error response-map entry
114
+ - `ctx.success(status, body)` for a declared non-default success response-map
115
+ entry
116
+ - a response-plugin value, such as an NDJSON iterable, when the route response
117
+ marker and router plugin match
118
+
119
+ Return a `Response` when you need custom status codes, headers, redirects,
120
+ non-JSON payloads, or hand-written error bodies.
121
+
122
+ ## Response Maps In Handlers
123
+
124
+ Routes with a status-keyed response map add `ctx.error` and `ctx.success`
125
+ helpers.
126
+
127
+ ```ts
128
+ export const getUser = http.get('users/:id', {
129
+ response: {
130
+ 200: $type<User>(),
131
+ 201: $type<User>(),
132
+ 404: $error<NotFound>(),
133
+ },
134
+ })
135
+
136
+ createRouter().use(
137
+ { getUser },
138
+ {
139
+ getUser(ctx) {
140
+ if (ctx.path.id === 'created') {
141
+ return ctx.success(201, { id: 'created', name: 'Grace' })
142
+ }
143
+
144
+ const user = users.get(ctx.path.id)
145
+ if (!user) {
146
+ return ctx.error(404, {
147
+ code: 'NOT_FOUND',
148
+ message: 'User not found',
149
+ })
150
+ }
151
+
152
+ return user
153
+ },
154
+ }
155
+ )
156
+ ```
157
+
158
+ The helpers only accept statuses and body types declared in the response map.
159
+
160
+ ## Router Configuration
161
+
162
+ `createRouter(config)` accepts:
163
+
164
+ | Option | Purpose |
165
+ | ------------------- | -------------------------------------------------------------------------------------------------------------- |
166
+ | `basePath` | Prepends a normalized path prefix to all route patterns. |
167
+ | `debug` | Adds matched-route debug headers, logs missing route handlers, and includes more specific validation messages. |
168
+ | `plugins` | Router response plugins such as `ndjson.routerPlugin`. |
169
+ | `cors.allowOrigins` | Restricts requests with an `Origin` header. |
170
+
171
+ ```ts
172
+ createRouter({
173
+ basePath: 'api/',
174
+ debug: process.env.NODE_ENV === 'development',
175
+ cors: {
176
+ allowOrigins: ['example.net', 'https://*.example.com'],
177
+ },
178
+ })
179
+ ```
180
+
181
+ When an allowed request includes an `Origin` header, Rouzer sets
182
+ `Access-Control-Allow-Origin` to that origin. For preflight requests, Rouzer
183
+ returns `Access-Control-Allow-Origin`, `Access-Control-Allow-Methods`, and
184
+ `Access-Control-Allow-Headers`.
185
+
186
+ Rouzer does not automatically set `Access-Control-Allow-Credentials`. Set that
187
+ header yourself when credentialed cross-origin requests need it.
188
+
189
+ ## Middleware Ordering
190
+
191
+ Attach middleware before route handlers that depend on it.
192
+
193
+ ```ts
194
+ createRouter().use(authMiddleware).use(routes, secureHandlers)
195
+ ```
196
+
197
+ Middleware attached after a route tree only runs if an earlier route handler
198
+ does not produce a response. Most applications put shared middleware before
199
+ route registration.
200
+
201
+ Use a middleware `Response` return for cross-cutting rejection, such as auth or
202
+ rate limits. Use route handlers for route-specific application results.
package/docs/index.md ADDED
@@ -0,0 +1,78 @@
1
+ # Rouzer documentation
2
+
3
+ Rouzer combines a shared TypeScript route tree, a fetch-compatible server
4
+ router, a typed fetch client, and request context composition. These guides are
5
+ organized by concern so the docs directory is the complete entry point for what
6
+ you can do and how the pieces fit together.
7
+
8
+ ## Learning Path
9
+
10
+ 1. Read [Framework concepts](concepts.md) for the lifecycle and boundaries.
11
+ 2. Read [Route contracts](routes.md) to declare resources, actions, schemas, and
12
+ metadata.
13
+ 3. Read [Middleware and request context](middleware.md) before writing shared
14
+ context, auth, environment, tracing, or runtime-specific middleware.
15
+ 4. Read [Routers and handlers](handlers.md) to attach route trees and implement
16
+ handlers.
17
+ 5. Read [Typed client](client.md) to call the same route tree from application
18
+ code.
19
+ 6. Read [Responses, errors, and plugins](responses.md) and
20
+ [NDJSON streaming](streaming.md) when routes need more than a simple JSON
21
+ success body.
22
+ 7. Read [Runtime and adapters](runtime.md) when mounting Rouzer in a server or
23
+ test harness.
24
+ 8. Read [Migration from v5 to v6](migration-v5-to-v6.md) when upgrading
25
+ from the Hattip-compatible server boundary.
26
+ 9. Read [Patterns, constraints, and migrations](patterns.md) for conventions,
27
+ gotchas, and upgrade notes.
28
+
29
+ ## Guide Map
30
+
31
+ | Guide | Concern |
32
+ | ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
33
+ | [Framework concepts](concepts.md) | The high-level model, request lifecycle, and Rouzer's framework responsibilities. |
34
+ | [Route contracts](routes.md) | `rouzer/http` resources, actions, Zod schemas, raw bodies, path patterns, and metadata. |
35
+ | [Middleware and request context](middleware.md) | `chain`, request plugins, `RequestContext`, `context.host`, env access, `waitUntil`, `onResponse`, `passThrough`, isolation, and runtime filtering. |
36
+ | [Routers and handlers](handlers.md) | `createRouter`, route handler maps, validated handler context, CORS, debug mode, middleware ordering, and handler return values. |
37
+ | [Typed client](client.md) | `createClient`, generated action functions, flat input objects, headers, custom fetch, `onJsonError`, and lifecycle hooks. |
38
+ | [Responses, errors, and plugins](responses.md) | `$type`, `$error`, response maps, status tuples, custom `Response` returns, and response plugin contracts. |
39
+ | [NDJSON streaming](streaming.md) | Streaming response markers, router/client plugin registration, cancellation, and stream error modeling. |
40
+ | [Runtime and adapters](runtime.md) | Root `toFetchHandler`, fetch-compatible mounting, custom host data, tests, CORS, and background work. |
41
+ | [Patterns, constraints, and migrations](patterns.md) | Preferred project structure, common constraints, and migration notes from older Rouzer shapes. |
42
+ | [Migration from v5 to v6](migration-v5-to-v6.md) | Hattip-compatible to fetch-compatible server mounting, type, test, host data, and runtime-filter updates. |
43
+
44
+ ## Request Lifecycle
45
+
46
+ 1. A shared route module exports resources and actions.
47
+ 2. Server code creates a router, appends middleware, and attaches the route tree
48
+ with a handler map.
49
+ 3. Runtime code mounts the router with `toFetchHandler` or an adapter-specific
50
+ helper.
51
+ 4. Client code creates a generated client from the same route tree.
52
+ 5. A client action validates route input, builds a `fetch` request, and sends
53
+ it.
54
+ 6. The router matches the request, validates path/query/body/header data, and
55
+ calls the typed handler.
56
+ 7. The handler returns JSON data, a custom `Response`, a declared response-map
57
+ helper, or a response-plugin value such as an NDJSON source.
58
+ 8. Response callbacks registered by middleware can finalize headers or replace
59
+ the response before it leaves the chain.
60
+
61
+ ## Framework Surface
62
+
63
+ Rouzer owns:
64
+
65
+ - route tree declarations and handler/client type inference
66
+ - request validation from route schemas
67
+ - route matching and handler dispatch
68
+ - response markers, response maps, and response plugin integration
69
+ - generated client action functions
70
+
71
+ Rouzer middleware and runtime helpers cover:
72
+
73
+ - middleware chaining and short-circuit behavior
74
+ - request context creation and extension
75
+ - host data such as `context.host.ip` and `context.host.runtime`
76
+ - typed environment access through `context.env(...)`
77
+ - `waitUntil`, `setHeader`, `onResponse`, `passThrough`, and chain isolation
78
+ - adapter helpers such as `toFetchHandler`
@@ -0,0 +1,185 @@
1
+ # Middleware and request context
2
+
3
+ Rouzer includes request context and middleware composition in its root API, so
4
+ most applications can import route, middleware, and adapter helpers from one
5
+ package.
6
+
7
+ ```ts
8
+ import {
9
+ chain,
10
+ filterRuntime,
11
+ type Middleware,
12
+ type MiddlewareContext,
13
+ type RequestContext,
14
+ type RequestHandler,
15
+ } from 'rouzer'
16
+ ```
17
+
18
+ ## Middleware Chain
19
+
20
+ `chain()` creates a `MiddlewareChain`. Rouzer routers are middleware chains too,
21
+ so you can append middleware before attaching routes.
22
+
23
+ ```ts
24
+ const requestInfo = chain().use(ctx => ({
25
+ requestId: ctx.request.headers.get('x-request-id') ?? crypto.randomUUID(),
26
+ }))
27
+
28
+ const router = createRouter()
29
+ .use(requestInfo)
30
+ .use(routes, {
31
+ getProfile(ctx) {
32
+ return loadProfile(ctx.path.id, ctx.requestId)
33
+ },
34
+ })
35
+ ```
36
+
37
+ Middleware runs in order. A middleware can:
38
+
39
+ - return `void` or `undefined` to continue without adding context
40
+ - return a `Response` to stop the chain and send that response
41
+ - return a request plugin object whose properties are merged into the downstream
42
+ context
43
+ - call `ctx.passThrough()` to skip the rest of the current chain
44
+
45
+ ## Request Context
46
+
47
+ Every middleware and Rouzer handler receives a `RequestContext`.
48
+
49
+ | Property | Purpose |
50
+ | ---------------------------- | ----------------------------------------------------------------------------- |
51
+ | `ctx.request` | The Web `Request`. |
52
+ | `ctx.url` | Lazily parsed `URL` for `ctx.request.url`. |
53
+ | `ctx.host` | Host data such as `ip`, `runtime`, `env`, and `waitUntil`. |
54
+ | `ctx.env(name)` | Typed environment lookup backed by `ctx.host.env` and middleware env plugins. |
55
+ | `ctx.waitUntil(promise)` | Delegates background work to `ctx.host.waitUntil` when available. |
56
+ | `ctx.setHeader(name, value)` | Sets a response header from request middleware. |
57
+ | `ctx.onResponse(callback)` | Registers a callback that can mutate or replace the final response. |
58
+ | `ctx.passThrough()` | Chain-local control flow for unresolved requests. |
59
+
60
+ Host-provided values live under `ctx.host`.
61
+
62
+ ```ts
63
+ const runtimeName = ctx.host.runtime?.name
64
+ const ip = ctx.host.ip
65
+ ```
66
+
67
+ Runtime-specific request data should stay behind `ctx.host.runtime`.
68
+
69
+ ## Adding Context Properties
70
+
71
+ Return a plain object to make properties available to downstream middleware and
72
+ handlers.
73
+
74
+ ```ts
75
+ const sessionMiddleware = chain().use(async ctx => {
76
+ const token = ctx.request.headers.get('authorization')
77
+ const session = token ? await readSession(token) : null
78
+
79
+ if (!session) {
80
+ return new Response('Unauthorized', { status: 401 })
81
+ }
82
+
83
+ return { session }
84
+ })
85
+ ```
86
+
87
+ Attach the middleware before route handlers that need the property.
88
+
89
+ ```ts
90
+ createRouter()
91
+ .use(sessionMiddleware)
92
+ .use(routes, {
93
+ me(ctx) {
94
+ return ctx.session.user
95
+ },
96
+ })
97
+ ```
98
+
99
+ Treat context property names as owned by the middleware that introduces them.
100
+ Avoid collisions between unrelated middleware.
101
+
102
+ ## Environment Bindings
103
+
104
+ Return an `env` object to add typed variables behind `ctx.env(...)`.
105
+
106
+ ```ts
107
+ const envMiddleware = chain().use(() => ({
108
+ env: {
109
+ DATABASE_URL: process.env.DATABASE_URL,
110
+ },
111
+ }))
112
+
113
+ const router = createRouter()
114
+ .use(envMiddleware)
115
+ .use(routes, {
116
+ health(ctx) {
117
+ return { configured: Boolean(ctx.env('DATABASE_URL')) }
118
+ },
119
+ })
120
+ ```
121
+
122
+ The `env` key is reserved for environment bindings. Values are accessed through
123
+ `ctx.env(name)`, not as `ctx.env.DATABASE_URL`.
124
+
125
+ ## Runtime Type Markers
126
+
127
+ The `runtime` request plugin key is reserved as a type-level marker for
128
+ `ctx.host.runtime`; it does not add `ctx.runtime`.
129
+
130
+ ```ts
131
+ const nodeOnly = chain()
132
+ .use(filterRuntime<{ name: 'node' }>('node'))
133
+ .use(ctx => {
134
+ ctx.host.runtime?.name
135
+ })
136
+ ```
137
+
138
+ `filterRuntime(name)` checks `ctx.host.runtime?.name`. If the runtime name does
139
+ not match, it calls `ctx.passThrough()`.
140
+
141
+ ## Response Callbacks
142
+
143
+ Use `ctx.onResponse(callback)` or return `{ onResponse }` when middleware needs
144
+ to finalize a response after a route handler runs.
145
+
146
+ ```ts
147
+ const timing = chain().use(ctx => {
148
+ const startedAt = Date.now()
149
+
150
+ ctx.onResponse(response => {
151
+ response.headers.set('server-timing', `app;dur=${Date.now() - startedAt}`)
152
+ })
153
+ })
154
+ ```
155
+
156
+ Use `ctx.setHeader(name, value)` from request middleware for simple headers.
157
+ Inside response callbacks, mutate `response.headers` directly or return a new
158
+ `Response`.
159
+
160
+ ## Isolation
161
+
162
+ Use `.isolate()` when you want to run a chain for side effects or early
163
+ responses without leaking its plugin properties into the parent chain.
164
+
165
+ ```ts
166
+ const optionalAuth = chain()
167
+ .use(readOptionalSession)
168
+ .use(auditSession)
169
+ .isolate()
170
+
171
+ const router = createRouter().use(optionalAuth).use(routes, handlers)
172
+ ```
173
+
174
+ An isolated chain can still return a `Response`. Its context additions stay
175
+ inside the isolated chain.
176
+
177
+ ## Pass Through
178
+
179
+ `ctx.passThrough()` is chain-local control flow. In a nested or isolated chain,
180
+ it skips the rest of that chain and lets the parent chain continue. In a final
181
+ fetch handler, an unresolved request becomes the default `404 Not Found`
182
+ response.
183
+
184
+ Use `passThrough` for optional middleware branches or runtime filters. Use a
185
+ `Response` when you want to deliberately stop the request.