22
33Rouzer is for applications that want one TypeScript HTTP route tree to drive
44both the server and the client that calls it. A route tree combines URL
5- patterns, named actions, HTTP method schemas, and optional compile-time JSON or
6- NDJSON response types.
5+ patterns, named actions, HTTP method schemas, and optional compile-time success,
6+ error, or plugin response types.
77
88## When to use Rouzer
99
@@ -92,11 +92,45 @@ The HTTP action API models explicit operations. It does not expose the old
9292method-map ` ALL ` fallback route shape; declare the concrete methods your client
9393and server support.
9494
95- ### ` $type<T>() ` and ` ndjson.$type<T>() `
95+ ### Response markers and maps
9696
97- ` response: $type<T>() ` is a TypeScript-only marker for JSON response payloads.
98- It tells handlers and client action functions what response payload type to
99- expect, but Rouzer does not validate response bodies at runtime.
97+ ` response: $type<T>() ` is a TypeScript-only marker for JSON success payloads. It
98+ tells handlers and client action functions what payload type to expect, but
99+ Rouzer does not validate response bodies at runtime.
100+
101+ Use a status-keyed response map when callers need to branch on declared statuses:
102+
103+ ``` ts
104+ import { $error , $type } from ' rouzer'
105+ import * as http from ' rouzer/http'
106+
107+ type User = { id: string ; name: string }
108+ type NotFound = { code: ' NOT_FOUND' ; message: string }
109+
110+ export const getUser = http .get (' users/:id' , {
111+ response: {
112+ 200 : $type <User >(),
113+ 201 : $type <User >(),
114+ 404 : $error <NotFound >(),
115+ },
116+ })
117+ ```
118+
119+ Success entries use ` $type<T>() ` or a response plugin marker. Error entries use
120+ ` $error<T>() ` and are encoded as JSON. Generated client action functions resolve
121+ declared statuses as tuples:
122+
123+ - success: ` [null, value, status] `
124+ - error: ` [error, null, status] `
125+
126+ Declared error statuses do not reject the client promise. Undeclared statuses
127+ still go through ` onJsonError ` or throw the default error.
128+
129+ Handlers for response-map actions may return the default success value directly,
130+ use ` ctx.success(status, body) ` to choose a declared success status, or use
131+ ` ctx.error(status, body) ` to return a declared error status. The ` ctx.error ` and
132+ ` ctx.success ` helpers only accept statuses and bodies declared in the response
133+ map.
100134
101135` response: ndjson.$type<T>() ` is a TypeScript-only marker for newline-delimited
102136JSON response streams from the ` rouzer/ndjson ` subpath. Register
@@ -109,8 +143,8 @@ response body. Streamed items are parsed as JSON but are not validated against a
109143Zod schema.
110144
111145Actions without a ` response ` marker return a raw ` Response ` from client action
112- functions. Actions with ` response: $type<T>() ` use ` client.json(...) ` under the
113- hood and return parsed JSON typed as ` T ` .
146+ functions. Actions with ` response: $type<T>() ` return parsed JSON typed as ` T ` .
147+ Actions with a response map return the tuple union described by that map .
114148
115149### Response plugins
116150
@@ -121,10 +155,11 @@ matching runtime plugins. For NDJSON, those are `ndjson.$type<T>()`,
121155
122156The router plugin encodes non-` Response ` handler results into an HTTP ` Response ` .
123157The client plugin decodes successful HTTP responses for generated client action
124- functions. Rouzer validates plugin registration when routes are attached to a
125- router or client, so routes that use an unregistered response marker fail fast
126- instead of falling back to JSON. Response plugins do not automatically validate
127- response payloads unless the plugin itself implements validation.
158+ functions. Plugin markers can also be success entries in a status-keyed response
159+ map. Rouzer validates plugin registration when routes are attached to a router or
160+ client, so routes that use an unregistered response marker fail fast instead of
161+ falling back to JSON. Response plugins do not automatically validate response
162+ payloads unless the plugin itself implements validation.
128163
129164### Router
130165
@@ -157,6 +192,8 @@ Handlers receive a context typed from middleware plus the action schema:
157192- ` GET ` handlers receive ` ctx.path ` , ` ctx.query ` , and ` ctx.headers `
158193- mutation handlers receive ` ctx.path ` , ` ctx.body ` , and ` ctx.headers `
159194- handlers may return a plain JSON-serializable value or a ` Response `
195+ - response-map handlers can return a default success value directly or use
196+ ` ctx.success(status, body) ` and ` ctx.error(status, body) `
160197- ` ndjson.$type<T>() ` handlers return an ` Iterable<T> ` or ` AsyncIterable<T> `
161198 unless they return a custom ` Response `
162199- plain values are returned with ` Response.json(value) `
@@ -175,6 +212,8 @@ requests with an `Origin` header.
175212 request factory contains the full path you want to call
176213- ` client.json(action.request(args)) ` for parsed JSON and default non-2xx
177214 throwing
215+ - response-map support for generated client action functions, returning
216+ ` [error, value, status] ` tuples for declared statuses
178217- response plugin support for generated client action functions, such as
179218 ` ndjson.clientPlugin ` for NDJSON response streams
180219- a client tree that mirrors ` routes ` , with action functions such as
@@ -205,9 +244,9 @@ runtimes.
205244 ` fetch ` .
2062455 . The router matches the request, validates the matched inputs, and calls the
207246 handler.
208- 6 . Plain handler results become JSON responses, plugin handler results become
209- plugin-encoded responses, and explicit ` Response ` objects pass through
210- unchanged.
247+ 6 . Plain handler results become JSON responses, response-map helpers choose
248+ declared statuses, plugin handler results become plugin-encoded responses, and
249+ explicit ` Response ` objects pass through unchanged.
211250
212251On the server, ` path ` , ` query ` , and ` headers ` values originate as strings. Rouzer
213252coerces Zod ` number ` schemas with ` Number(value) ` and Zod ` boolean ` schemas from
@@ -247,9 +286,64 @@ const json = await client.json(
247286)
248287```
249288
250- Response plugins are applied by generated client action functions. For longhand
251- calls to plugin-backed routes, use ` client.request(...) ` for the raw ` Response `
252- and call the plugin subpath's decoder yourself.
289+ Response maps and response plugins are applied by generated client action
290+ functions. For longhand calls to mapped or plugin-backed routes, use
291+ ` client.request(...) ` for the raw ` Response ` and decode the response yourself.
292+
293+ ### Handle declared error responses
294+
295+ Use ` $error<T>() ` inside a response map when an error status is part of the route
296+ contract:
297+
298+ ``` ts
299+ import { $error , $type , createClient , createRouter } from ' rouzer'
300+ import * as http from ' rouzer/http'
301+
302+ type User = { id: string ; name: string }
303+ type NotFound = { code: ' NOT_FOUND' ; message: string }
304+
305+ export const getUser = http .get (' users/:id' , {
306+ response: {
307+ 200 : $type <User >(),
308+ 404 : $error <NotFound >(),
309+ },
310+ })
311+ export const routes = { getUser }
312+
313+ createRouter ().use (routes , {
314+ getUser(ctx ) {
315+ if (ctx .path .id === ' missing' ) {
316+ return ctx .error (404 , {
317+ code: ' NOT_FOUND' ,
318+ message: ' User not found' ,
319+ })
320+ }
321+ return { id: ctx .path .id , name: ' Ada' }
322+ },
323+ })
324+
325+ const client = createClient ({
326+ baseURL: ' https://example.com/api/' ,
327+ routes ,
328+ })
329+
330+ const [error, user, status] = await client .getUser ({
331+ path: { id: ' missing' },
332+ })
333+
334+ if (status === 404 ) {
335+ console .log (error .message )
336+ } else {
337+ console .log (user .name )
338+ }
339+ ```
340+
341+ A complete runnable version lives in
342+ [ ` examples/error-responses.ts ` ] ( ../examples/error-responses.ts ) .
343+
344+ When a response map declares multiple success statuses, return a plain value for
345+ the default success status or use ` ctx.success(status, body) ` to choose a
346+ specific declared success status.
253347
254348### Stream newline-delimited JSON
255349
@@ -322,8 +416,8 @@ custom headers. Return a plain value for the default `Response.json(value)` path
322416### Customize JSON errors
323417
324418By default, ` client.json(...) ` and generated client action functions throw for
325- non-2xx responses. If the response body is JSON, its properties are copied onto
326- the thrown ` Error ` .
419+ non-2xx responses that are not declared in a response map. If the response body
420+ is JSON, its properties are copied onto the thrown ` Error ` .
327421
328422` onJsonError ` can override that behavior. Its return value is returned from the
329423response helper as-is; Rouzer does not automatically parse a returned ` Response `
@@ -390,6 +484,8 @@ await client.profiles.update({
390484 only when string params are sufficient.
391485- Use ` response: $type<T>() ` for JSON endpoints that should have typed client
392486 action functions.
487+ - Use response maps with ` $error<T>() ` when callers should handle declared error
488+ statuses as typed data instead of exceptions.
393489- Use ` response: ndjson.$type<T>() ` plus ` ndjson.routerPlugin ` and
394490 ` ndjson.clientPlugin ` for response streams where each line is a JSON value and
395491 the client should consume an ` AsyncIterable<T> ` .
@@ -400,10 +496,12 @@ await client.profiles.update({
400496
401497## Constraints and gotchas
402498
403- - ` $type<T>() ` and ` ndjson.$type<T>() ` are compile-time only and do not validate
404- response payloads or streamed items.
499+ - ` $type<T>() ` , ` $error<T>() ` , and ` ndjson.$type<T>() ` are compile-time only and
500+ do not validate response payloads or streamed items.
405501- NDJSON support is for response streams; request bodies still use the existing
406502 JSON body schema path.
503+ - Declared ` $error<T>() ` responses are JSON responses. Use a custom ` Response `
504+ for non-JSON error payloads.
407505- Routes that use a response plugin fail fast if the matching client or router
408506 plugin is not registered.
409507- Pathname route patterns expect an absolute client ` baseURL ` .
0 commit comments