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.
@@ -0,0 +1,205 @@
1
+ # Migrating from v5 to v6
2
+
3
+ > Move a v5 application from Hattip-compatible server integration to Rouzer's
4
+ > Fetch-compatible handler boundary without rewriting its route contract.
5
+
6
+ Rouzer v6 moves the server runtime boundary from Hattip-compatible handlers to
7
+ fetch-compatible Rouzer handlers. Route declarations, handler maps, generated
8
+ clients, response maps, response plugins, raw bodies, and NDJSON routes keep the
9
+ same Rouzer shape.
10
+
11
+ > [!IMPORTANT]
12
+ > Most applications only need to update server mounting code, tests, and any
13
+ > explicit Hattip handler or context types. Keep the shared route tree unchanged
14
+ > unless the application API is changing too.
15
+
16
+ ## Replace Hattip adapters with Fetch handlers
17
+
18
+ Remove Hattip packages that were used only to mount Rouzer handlers.
19
+
20
+ ```sh
21
+ pnpm remove @hattip/core @hattip/adapter-node @hattip/adapter-test
22
+ pnpm add rouzer zod
23
+ ```
24
+
25
+ Use Rouzer's root `toFetchHandler` helper when a server or test harness expects
26
+ a function that receives a Web `Request` and returns a `Response`.
27
+
28
+ ```ts
29
+ import { createRouter, toFetchHandler } from 'rouzer'
30
+
31
+ const router = createRouter({ basePath: 'api/' }).use(routes, handlers)
32
+
33
+ export const fetch = toFetchHandler(router)
34
+ ```
35
+
36
+ If your server already accepts a Fetch API handler, mount `fetch` directly. If
37
+ your server exposes host metadata such as client IP, runtime data, environment
38
+ lookups, or background work, pass it through `toFetchHandler`.
39
+
40
+ ```ts
41
+ const fetch = toFetchHandler(router, {
42
+ host: request => ({
43
+ ip: request.headers.get('x-forwarded-for') ?? undefined,
44
+ runtime: { name: 'node' },
45
+ env: name => process.env[name],
46
+ waitUntil: promise => {
47
+ void promise
48
+ },
49
+ }),
50
+ })
51
+ ```
52
+
53
+ ## Replace Hattip types
54
+
55
+ Rouzer no longer requires Hattip types in application code.
56
+
57
+ ```ts
58
+ // v5
59
+ import type { AdapterRequestContext, HattipHandler } from '@hattip/core'
60
+
61
+ // v6
62
+ import type { RequestContext, RequestHandler } from 'rouzer'
63
+ ```
64
+
65
+ Rouzer routers are `RequestHandler` values. Middleware and route handlers receive
66
+ `RequestContext` plus the route-specific fields that Rouzer infers from your
67
+ route tree.
68
+
69
+ ## Update tests
70
+
71
+ If tests used `@hattip/adapter-test`, replace it with a small local fetch
72
+ wrapper.
73
+
74
+ ```ts
75
+ // v5
76
+ import { createTestClient } from '@hattip/adapter-test'
77
+
78
+ const client = createClient({
79
+ baseURL,
80
+ routes,
81
+ fetch: createTestClient({
82
+ handler,
83
+ baseUrl: baseURL,
84
+ }),
85
+ })
86
+ ```
87
+
88
+ ```ts
89
+ // v6
90
+ import { createClient, 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
+ const client = createClient({
98
+ baseURL,
99
+ routes,
100
+ fetch: createLocalFetch(handler),
101
+ })
102
+ ```
103
+
104
+ The wrapper matters because generated clients call `fetch(input, init)`, while a
105
+ Rouzer fetch handler receives one `Request`.
106
+
107
+ ## Use `ctx.host` for host data
108
+
109
+ Host-provided values now live under `ctx.host`.
110
+
111
+ ```ts
112
+ const ip = ctx.host.ip
113
+ const runtimeName = ctx.host.runtime?.name
114
+ ```
115
+
116
+ If old code read adapter-specific fields directly from the context, move those
117
+ values behind the `host` option when creating the fetch handler.
118
+
119
+ ```ts
120
+ const fetch = toFetchHandler(router, {
121
+ host: request => ({
122
+ ip: request.headers.get('x-forwarded-for') ?? undefined,
123
+ runtime: { name: 'custom' },
124
+ }),
125
+ })
126
+ ```
127
+
128
+ ## Rename runtime filters
129
+
130
+ If code used the old platform filter helper from Rouzer's root exports, use
131
+ `filterRuntime`.
132
+
133
+ ```ts
134
+ // v5
135
+ import { filterPlatform } from 'rouzer'
136
+
137
+ const middleware = chain().use(filterPlatform('node'))
138
+ ```
139
+
140
+ ```ts
141
+ // v6
142
+ import { filterRuntime } from 'rouzer'
143
+
144
+ const middleware = chain().use(filterRuntime<{ name: 'node' }>('node'))
145
+ ```
146
+
147
+ `filterRuntime(name)` checks `ctx.host.runtime?.name` and narrows the downstream
148
+ runtime type.
149
+
150
+ ## Update custom contexts
151
+
152
+ If tests or custom adapters constructed request contexts directly, use
153
+ `createContext` with a Web `Request` and optional `host` data.
154
+
155
+ ```ts
156
+ import { createContext } from 'rouzer'
157
+
158
+ const context = createContext({
159
+ request: new Request('https://example.test/api/health'),
160
+ host: {
161
+ ip: '127.0.0.1',
162
+ runtime: { name: 'test' },
163
+ env: name => process.env[name],
164
+ waitUntil: promise => {
165
+ void promise
166
+ },
167
+ },
168
+ })
169
+
170
+ const response = await router(context)
171
+ ```
172
+
173
+ ## Update pass-through assumptions
174
+
175
+ `ctx.passThrough()` is middleware-chain control flow. It skips the rest of the
176
+ current chain and lets the parent chain continue. In a final fetch handler, an
177
+ unresolved request becomes Rouzer's default `404 Not Found` response.
178
+
179
+ Use `ctx.passThrough()` for optional middleware branches and runtime filters.
180
+ Return a `Response` when you want to stop the request deliberately.
181
+
182
+ ## What stays the same
183
+
184
+ These Rouzer APIs keep the same shape:
185
+
186
+ - `rouzer/http` resources and actions
187
+ - `createRouter().use(routes, handlers)` route registration
188
+ - middleware added with `.use(middleware)`
189
+ - generated clients from `createClient({ baseURL, routes })`
190
+ - flat client action input objects
191
+ - `$type<T>()`, `$error<T>()`, and response maps
192
+ - response plugins such as `ndjson.routerPlugin` and `ndjson.clientPlugin`
193
+ - `http.rawBody()` request bodies
194
+
195
+ ## Checklist
196
+
197
+ 1. Remove Hattip packages that were only used for Rouzer serving or tests.
198
+ 2. Mount routers with `toFetchHandler(router)`.
199
+ 3. Replace Hattip handler/context types with Rouzer `RequestHandler` and
200
+ `RequestContext`.
201
+ 4. Replace test adapters with a local fetch wrapper.
202
+ 5. Move host metadata into the `host` option and read it from `ctx.host`.
203
+ 6. Replace platform filters with `filterRuntime`.
204
+ 7. Keep route trees, handler maps, clients, responses, and plugins unchanged
205
+ unless your application code was also changing them.
@@ -0,0 +1,147 @@
1
+ # Patterns, constraints, and migrations
2
+
3
+ > Review the stable project patterns and sharp edges that matter when route
4
+ > contracts, middleware, handlers, and clients are composed in one application.
5
+
6
+ Use this page as a checklist when designing route modules, middleware, and
7
+ client usage.
8
+
9
+ > [!IMPORTANT]
10
+ > For the v5 server-boundary upgrade, follow
11
+ > [Migration from v5 to v6](migration-v5-to-v6.md) instead of treating this
12
+ > checklist as a migration procedure.
13
+
14
+ ## Preferred Patterns
15
+
16
+ - Export route trees from small shared modules and import them from server and
17
+ client code.
18
+ - Keep route declarations close to domain boundaries, not scattered through
19
+ handler implementations.
20
+ - Use `http.resource(...)` for resource namespaces and shared path params.
21
+ - Name actions after domain operations such as `get`, `list`, `update`,
22
+ `archive`, and `stream`.
23
+ - Let `http.get`, `http.post`, `http.put`, `http.patch`, and `http.delete` own
24
+ the transport method.
25
+ - Add Zod schemas when runtime guarantees matter; rely on inferred path params
26
+ only when string params are enough.
27
+ - Use `$type<T>()` for typed JSON success responses.
28
+ - Use response maps with `$error<T>()` for declared application errors that
29
+ callers should handle as data.
30
+ - Use `Response` returns for redirects, binary payloads, custom non-JSON error
31
+ bodies, or unusual headers.
32
+ - Use `ndjson.$type<T>()` only for response streams where each line is a JSON
33
+ value.
34
+ - Put shared auth, tracing, request IDs, environment bindings, and host-runtime
35
+ checks in middleware before route registration.
36
+
37
+ ## Constraints And Gotchas
38
+
39
+ - `$type<T>()`, `$error<T>()`, and `ndjson.$type<T>()` are compile-time type
40
+ contracts. They do not re-validate handler return values.
41
+ - Client action input is flat across path, query, and JSON body fields. Avoid
42
+ duplicate field names across those schemas.
43
+ - Per-request `RequestInit` fields belong in the second client action argument.
44
+ Rouzer reserves `method`.
45
+ - Raw-body actions with path or query input accept `body` in the second
46
+ argument. Raw-body actions without route input accept the body as the first
47
+ argument.
48
+ - `GET` actions do not accept request bodies. Mutation actions do not accept
49
+ query schemas.
50
+ - The action API has no `ALL` fallback route. Declare concrete actions for
51
+ supported HTTP methods.
52
+ - Pathname route patterns expect an absolute client `baseURL`.
53
+ - Resource and action keys are API names only. URL paths come from the pattern
54
+ strings passed to resources and actions.
55
+ - Routes that use response plugin markers require matching router and client
56
+ plugins.
57
+ - Declared `$error<T>()` responses are JSON responses. Use a custom `Response`
58
+ for non-JSON error payloads.
59
+ - Rouzer CORS support does not set `Access-Control-Allow-Credentials`.
60
+
61
+ ## Middleware Constraints
62
+
63
+ - Host data is under `ctx.host`, including `ctx.host.ip` and
64
+ `ctx.host.runtime`.
65
+ - The `env`, `runtime`, and `onResponse` request plugin keys are reserved.
66
+ - `runtime` is a type-level marker for `ctx.host.runtime`; it does not create
67
+ `ctx.runtime`.
68
+ - Use `ctx.setHeader` from request middleware and `response.headers.set(...)`
69
+ inside response callbacks.
70
+ - `ctx.passThrough()` skips the rest of the current chain. It is not an adapter
71
+ pass-through mechanism.
72
+ - Use `.isolate()` when a chain should run without leaking context properties to
73
+ the parent chain.
74
+
75
+ ## Project Shape
76
+
77
+ A common layout:
78
+
79
+ ```text
80
+ src/
81
+ routes/
82
+ profiles.ts
83
+ server/
84
+ middleware.ts
85
+ router.ts
86
+ client/
87
+ api.ts
88
+ ```
89
+
90
+ `routes/profiles.ts` exports route contracts. `server/router.ts` imports those
91
+ contracts and attaches handlers. `client/api.ts` imports the same contracts and
92
+ creates the typed client.
93
+
94
+ > [!IMPORTANT]
95
+ > Keep shared route modules free of server-only dependencies when browser
96
+ > clients import them.
97
+
98
+ ## v2 To v3 Route Shape
99
+
100
+ Older Rouzer code used method-map routes. Current Rouzer uses action/resource
101
+ route trees.
102
+
103
+ ```ts
104
+ // Old shape
105
+ export const profileRoute = route('profiles/:id', {
106
+ GET: { response: $type<Profile>() },
107
+ PATCH: { body: updateProfileSchema, response: $type<Profile>() },
108
+ })
109
+ ```
110
+
111
+ Use named actions instead:
112
+
113
+ ```ts
114
+ export const profiles = http.resource('profiles/:id', {
115
+ get: http.get({ response: $type<Profile>() }),
116
+ update: http.patch({
117
+ body: updateProfileSchema,
118
+ response: $type<Profile>(),
119
+ }),
120
+ })
121
+
122
+ export const routes = { profiles }
123
+ ```
124
+
125
+ Handler maps and clients mirror those action names.
126
+
127
+ ```ts
128
+ createRouter().use(routes, {
129
+ profiles: {
130
+ get(ctx) {
131
+ return loadProfile(ctx.path.id)
132
+ },
133
+ update(ctx) {
134
+ return updateProfile(ctx.path.id, ctx.body)
135
+ },
136
+ },
137
+ })
138
+
139
+ await client.profiles.get({ id: '42' })
140
+ await client.profiles.update({ id: '42', name: 'Ada' })
141
+ ```
142
+
143
+ ## Runtime Context Notes
144
+
145
+ Use `ctx.host.runtime` for host runtime metadata and `filterRuntime(...)` for
146
+ runtime-specific branches. Keep runtime checks at the middleware boundary so
147
+ route handlers stay focused on request validation and application behavior.
@@ -0,0 +1,191 @@
1
+ # Responses, errors, and plugins
2
+
3
+ > Choose whether callers receive typed JSON, a raw Web `Response`, a declared
4
+ > status tuple, or a plugin-decoded value.
5
+
6
+ Rouzer response schemas shape handler return types and generated client result
7
+ types.
8
+
9
+ > [!IMPORTANT]
10
+ > Response markers do not validate handler return values at runtime. They are
11
+ > TypeScript contracts between handlers and generated clients.
12
+
13
+ ## Plain JSON Success
14
+
15
+ Use `$type<T>()` for JSON responses that should be typed on the server and
16
+ client.
17
+
18
+ ```ts
19
+ export const getProfile = http.get('profiles/:id', {
20
+ response: $type<Profile>(),
21
+ })
22
+
23
+ createRouter().use(
24
+ { getProfile },
25
+ {
26
+ getProfile(ctx) {
27
+ return { id: ctx.path.id, name: 'Ada' }
28
+ },
29
+ }
30
+ )
31
+
32
+ const profile = await client.getProfile({ id: '42' })
33
+ ```
34
+
35
+ Handlers may return a plain JSON-serializable value or a custom `Response`.
36
+ Plain values are sent with `Response.json(value)`.
37
+
38
+ ## Raw Response
39
+
40
+ If an action has no `response` marker, generated client action functions resolve
41
+ to the raw `Response`.
42
+
43
+ ```ts
44
+ export const download = http.get('exports/:id', {})
45
+
46
+ const response = await client.download({ id: 'exp_123' })
47
+ const blob = await response.blob()
48
+ ```
49
+
50
+ Return a `Response` from handlers for redirects, custom headers, binary bodies,
51
+ non-JSON content, or custom status handling.
52
+
53
+ ## Declared Error Responses
54
+
55
+ Use `$error<T>()` inside a response map when an error status is part of the route
56
+ contract and callers should handle it as typed data instead of an exception.
57
+
58
+ ```ts
59
+ type User = { id: string; name: string }
60
+ type NotFound = { code: 'NOT_FOUND'; message: string }
61
+
62
+ export const getUser = http.get('users/:id', {
63
+ response: {
64
+ 200: $type<User>(),
65
+ 404: $error<NotFound>(),
66
+ },
67
+ })
68
+ ```
69
+
70
+ Response-map handlers can return a default success value directly or use helper
71
+ methods.
72
+
73
+ ```ts
74
+ createRouter().use(
75
+ { getUser },
76
+ {
77
+ getUser(ctx) {
78
+ const user = users.get(ctx.path.id)
79
+ if (!user) {
80
+ return ctx.error(404, {
81
+ code: 'NOT_FOUND',
82
+ message: 'User not found',
83
+ })
84
+ }
85
+ return user
86
+ },
87
+ }
88
+ )
89
+ ```
90
+
91
+ Generated clients resolve declared statuses as tuples:
92
+
93
+ - success: `[null, value, status]`
94
+ - error: `[error, null, status]`
95
+
96
+ ```ts
97
+ const [error, user, status] = await client.getUser({ id: 'missing' })
98
+
99
+ if (status === 404) {
100
+ console.log(error.message)
101
+ } else {
102
+ console.log(user.name)
103
+ }
104
+ ```
105
+
106
+ Declared error statuses do not reject the client promise. Undeclared statuses
107
+ still go through `onJsonError` or throw the default error.
108
+
109
+ ## Multiple Success Statuses
110
+
111
+ When a response map declares multiple success statuses, return a plain value for
112
+ the default success status or use `ctx.success(status, body)` to choose a
113
+ specific declared success status.
114
+
115
+ ```ts
116
+ export const createUser = http.post('users', {
117
+ body: createUserSchema,
118
+ response: {
119
+ 200: $type<User>(),
120
+ 201: $type<User>(),
121
+ 409: $error<Conflict>(),
122
+ },
123
+ })
124
+
125
+ createRouter().use(
126
+ { createUser },
127
+ {
128
+ createUser(ctx) {
129
+ if (alreadyExists(ctx.body.email)) {
130
+ return ctx.error(409, {
131
+ code: 'CONFLICT',
132
+ message: 'Email already exists',
133
+ })
134
+ }
135
+
136
+ return ctx.success(201, create(ctx.body))
137
+ },
138
+ }
139
+ )
140
+ ```
141
+
142
+ The helpers only accept statuses and bodies declared in the map.
143
+
144
+ ## Response Plugins
145
+
146
+ Response plugins add non-JSON response codecs without changing route matching or
147
+ request validation. A plugin has:
148
+
149
+ - a compile-time response marker
150
+ - a router plugin that encodes handler results into `Response` objects
151
+ - a client plugin that decodes successful `Response` objects into client results
152
+
153
+ NDJSON is the built-in example:
154
+
155
+ ```ts
156
+ export const events = http.get('events', {
157
+ response: ndjson.$type<Event>(),
158
+ })
159
+
160
+ createRouter({
161
+ plugins: [ndjson.routerPlugin],
162
+ })
163
+
164
+ createClient({
165
+ baseURL,
166
+ routes,
167
+ plugins: [ndjson.clientPlugin],
168
+ })
169
+ ```
170
+
171
+ > [!IMPORTANT]
172
+ > Register the matching response plugin on both the router and client. Rouzer
173
+ > fails fast when attaching routes with an unregistered marker instead of
174
+ > falling back to JSON.
175
+
176
+ Plugin markers can be used directly as an action response or as success entries
177
+ in a response map. `$error<T>()` entries are JSON error responses.
178
+
179
+ ## Runtime Validation Boundary
180
+
181
+ `$type<T>()`, `$error<T>()`, and plugin markers are TypeScript contracts. Rouzer
182
+ does not re-check handler return values against those types at the server
183
+ boundary.
184
+
185
+ Validate untrusted data where it enters your system, such as:
186
+
187
+ - external API clients
188
+ - database decoders
189
+ - queue/event consumers
190
+ - form-data or file-processing layers
191
+ - UI/client boundaries that consume untrusted responses