Releases: alloc/rouzer
v5.1.0
Adds a more ergonomic client call signature for http.rawBody() routes that do not have path or query input.
Raw-body routes with route input continue to pass the payload through options.body:
export const uploadAvatar = http.post('profiles/:id/avatar', {
body: http.rawBody(),
})
await client.uploadAvatar({ id: '42' }, { body: file })Raw-body routes without route input can now pass the body as the first argument:
export const upload = http.post('uploads', {
body: http.rawBody(),
})
await client.upload(file, {
headers: { 'content-type': file.type },
})Also fixes the standalone RouteRequestHandlerMap default middleware type so handler contexts no longer collapse to any when no middleware parameter is supplied.
v5.0.0
Breaking changes
Flattened client action arguments
Generated client methods now take semantic route input as the first argument and fetch options as the second argument.
Before:
await client.hello({
path: { name: 'world' },
query: { excited: true },
headers: { 'x-request-id': 'abc' },
signal,
})After:
await client.hello(
{ name: 'world', excited: true },
{ headers: { 'x-request-id': 'abc' }, signal }
)Path params, query params, and JSON object body fields are flattened into the first input object. RequestInit options and request headers belong in the second options object.
This also updates client response plugin request metadata: ClientResponsePluginRequest now exposes input and options directly instead of nesting them under args.
Mutation body schemas must be Zod objects
JSON mutation body schemas are now restricted to z.ZodObject. This is required so the client can pick body fields out of the flattened input object before validating and JSON encoding the request body.
If you need to send a non-object payload, use the new http.rawBody() marker described below.
New features
Raw request bodies
Added http.rawBody() for routes that need to pass a body through to fetch without JSON encoding, such as binary uploads, images, streams, FormData, or text payloads.
import * as http from 'rouzer/http'
export const uploadAvatar = http.post('avatars/:id', {
body: http.rawBody(),
})Client usage:
await client.uploadAvatar(
{ id: 'user-123' },
{ body: imageBytes }
)Raw body routes:
- read path params from the first
inputargument - read the raw fetch body from
options.body - pass the body to
fetchunchanged - skip client-side JSON stringification
- skip server-side JSON body parsing, so handlers can read from
requestdirectly with APIs likearrayBuffer(),blob(),formData(), ortext()
Migration guide
Client calls with path/query/body
Replace nested path, query, and body objects with a single flat input object.
// v4
await client.users.update({
path: { id: '42' },
body: { name: 'Grace' },
})
// v5
await client.users.update({ id: '42', name: 'Grace' })Client calls with request options
Move request options to the second argument.
// v4
await client.search({
query: { q: 'rouzer' },
headers: { authorization: token },
signal,
})
// v5
await client.search(
{ q: 'rouzer' },
{ headers: { authorization: token }, signal }
)Request-level headers remain optional because headers can still be supplied as createClient({ headers }) defaults.
Non-object request bodies
Replace non-object Zod body schemas with http.rawBody() and pass the payload via options.body.
// v5
export const upload = http.post('upload', {
body: http.rawBody(),
})
await client.upload(undefined, { body: file })v4.0.0
What changed
Rouzer v4 removes the low-level client request descriptor API and makes generated, route-backed clients the only supported client shape.
Breaking changes
createClientnow requiresroutes.- Use
createClient({ baseURL, routes })and call generated action functions likeclient.users.get(...).
- Use
- HTTP actions no longer expose nested
.request(...)factories. - Generated clients no longer expose
client.request(...)orclient.json(...)helpers. - The client metadata property was renamed from
configtoclientConfig.- This allows route trees to define an action named
configwithout being overwritten, soclient.config(...)can be a route.
- This allows route trees to define an action named
Why
The removed request factory path was ambiguous for nested resources because action-local factories did not carry parent resource paths. Requiring createClient({ routes }) keeps URL construction, validation, response maps, and response plugins on the same generated action path.
Migration
Before:
const client = createClient({ baseURL })
await client.json(routes.getUser.request({ path: { id: '42' } }))After:
const client = createClient({ baseURL, routes })
await client.getUser({ path: { id: '42' } })If you read the client options from the returned client, use client.clientConfig instead of client.config.
Full Changelog: v3.2.0...v4.0.0
v3.2.0
Typed response maps.
import { $error, $type } from 'rouzer'
import * as http from 'rouzer/http'
export const getUser = http.get('users/:id', {
response: {
200: $type<User>(),
404: $error<{ code: 'NOT_FOUND'; message: string }>(),
},
})Handlers return declared errors with ctx.error:
createRouter().use({ getUser }, {
getUser(ctx) {
if (ctx.path.id === 'missing') {
return ctx.error(404, {
code: 'NOT_FOUND',
message: 'User not found',
})
}
return { id: ctx.path.id, name: 'Ada' }
},
})Clients get typed tuples instead of thrown errors for declared statuses:
const [error, user, status] = await client.getUser({
path: { id: 'missing' },
})Also:
- Add
$error<T>()for declared JSON error responses. - Add
ctx.success(status, body)for choosing explicit declared success statuses. - Support response plugins, including NDJSON, inside response maps.
- Forward extra
RequestInitfields such assignalandcredentialsfrom client calls. - Add runnable typed error response docs and tests.
Response markers are type contracts. Rouzer does not re-validate handler return values at the server boundary; validate data where it enters your server or client code.
Compare: v3.1.0...v3.2.0
v3.1.0
What's new
- Added NDJSON response stream support via the new
rouzer/ndjsonsubpath.- Define streamed responses with
ndjson.$type<T>(). - Register
ndjson.routerPluginwithcreateRouter(...)andndjson.clientPluginwithcreateClient(...). - Handlers can return
Iterable<T>orAsyncIterable<T>values, which Rouzer serializes asapplication/x-ndjson. - Generated client action functions resolve to an
AsyncIterable<T>parsed from the response body.
- Define streamed responses with
- Added response plugin support for non-JSON response codecs, with fast failure when plugin-backed routes are used without the matching router/client plugin.
- Added a runnable NDJSON streaming example:
examples/ndjson-stream.ts.
Notes
- Streamed NDJSON items are parsed as JSON but are not validated against a Zod schema.
- NDJSON support is for response streams; request bodies continue to use the existing JSON body schema path.
- Generated client action functions now follow the documented
client.json(...)non-2xx error behavior.
v3.0.0
Update every route map to rouzer/http.
-import { route } from 'rouzer'
+import * as http from 'rouzer/http'
-export const profileRoute = route('profiles/:id', {
- GET: { response: $type<Profile>() },
- PATCH: { body: updateProfileSchema, response: $type<Profile>() },
+export const profiles = http.resource('profiles/:id', {
+ get: http.get({ response: $type<Profile>() }),
+ update: http.patch({ body: updateProfileSchema, response: $type<Profile>() }),
}) createRouter().use(routes, {
- profileRoute: {
- GET(ctx) { ... },
- PATCH(ctx) { ... },
+ profiles: {
+ get(ctx) { ... },
+ update(ctx) { ... },
},
})-await client.profileRoute.GET({ path: { id: '42' } })
+await client.profiles.get({ path: { id: '42' } })Also:
createClient({ routes })andcreateRouter().use(...)now take HTTP action/resource trees.- Keep
route(...)only for low-levelclient.request(...)/client.json(...)calls. ALLfallback routes and route-levelOPTIONShandlers are removed.@remix-run/route-patternis upgraded to v0.21.
Docs: https://github.com/alloc/rouzer/blob/v3.0.0/docs/context.md
Compare: v2.0.1...v3.0.0
v2.0.0
- Breaking change: Switch from
zod/minito regularzod