Skip to content

Releases: alloc/rouzer

v5.1.0

26 May 16:14

Choose a tag to compare

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

26 May 14:12

Choose a tag to compare

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 input argument
  • read the raw fetch body from options.body
  • pass the body to fetch unchanged
  • skip client-side JSON stringification
  • skip server-side JSON body parsing, so handlers can read from request directly with APIs like arrayBuffer(), blob(), formData(), or text()

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

24 May 19:13

Choose a tag to compare

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

  • createClient now requires routes.
    • Use createClient({ baseURL, routes }) and call generated action functions like client.users.get(...).
  • HTTP actions no longer expose nested .request(...) factories.
  • Generated clients no longer expose client.request(...) or client.json(...) helpers.
  • The client metadata property was renamed from config to clientConfig.
    • This allows route trees to define an action named config without being overwritten, so client.config(...) can be a route.

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

24 May 15:05

Choose a tag to compare

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 RequestInit fields such as signal and credentials from 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

23 May 02:54

Choose a tag to compare

What's new

  • Added NDJSON response stream support via the new rouzer/ndjson subpath.
    • Define streamed responses with ndjson.$type<T>().
    • Register ndjson.routerPlugin with createRouter(...) and ndjson.clientPlugin with createClient(...).
    • Handlers can return Iterable<T> or AsyncIterable<T> values, which Rouzer serializes as application/x-ndjson.
    • Generated client action functions resolve to an AsyncIterable<T> parsed from the response body.
  • 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

22 May 20:31

Choose a tag to compare

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 }) and createRouter().use(...) now take HTTP action/resource trees.
  • Keep route(...) only for low-level client.request(...) / client.json(...) calls.
  • ALL fallback routes and route-level OPTIONS handlers are removed.
  • @remix-run/route-pattern is 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

19 Mar 18:47

Choose a tag to compare

  • Breaking change: Switch from zod/mini to regular zod