Migrating from v5 to v6

Move a v5 application from Hattip-compatible server integration to Rouzer's Fetch-compatible handler boundary without rewriting its route contract.

Rouzer v6 moves the server runtime boundary from Hattip-compatible handlers to fetch-compatible Rouzer handlers. Route declarations, handler maps, generated clients, response maps, response plugins, raw bodies, and NDJSON routes keep the same Rouzer shape.

Important

Most applications only need to update server mounting code, tests, and any explicit Hattip handler or context types. Keep the shared route tree unchanged unless the application API is changing too.

Replace Hattip adapters with Fetch handlers

Remove Hattip packages that were used only to mount Rouzer handlers.

pnpm remove @hattip/core @hattip/adapter-node @hattip/adapter-test
pnpm add rouzer zod

Use Rouzer's root toFetchHandler helper when a server or test harness expects a function that receives a Web Request and returns a Response.

import { createRouter, toFetchHandler } from 'rouzer'

const router = createRouter({ basePath: 'api/' }).use(routes, handlers)

export const fetch = toFetchHandler(router)

If your server already accepts a Fetch API handler, mount fetch directly. If your server exposes host metadata such as client IP, runtime data, environment lookups, or background work, pass it through toFetchHandler.

const fetch = toFetchHandler(router, {
  host: request => ({
    ip: request.headers.get('x-forwarded-for') ?? undefined,
    runtime: { name: 'node' },
    env: name => process.env[name],
    waitUntil: promise => {
      void promise
    },
  }),
})

Replace Hattip types

Rouzer no longer requires Hattip types in application code.

// v5
import type { AdapterRequestContext, HattipHandler } from '@hattip/core'

// v6
import type { RequestContext, RequestHandler } from 'rouzer'

Rouzer routers are RequestHandler values. Middleware and route handlers receive RequestContext plus the route-specific fields that Rouzer infers from your route tree.

Update tests

If tests used @hattip/adapter-test, replace it with a small local fetch wrapper.

// v5
import { createTestClient } from '@hattip/adapter-test'

const client = createClient({
  baseURL,
  routes,
  fetch: createTestClient({
    handler,
    baseUrl: baseURL,
  }),
})
// v6
import { createClient, toFetchHandler, type RequestHandler } from 'rouzer'

function createLocalFetch(handler: RequestHandler): typeof fetch {
  const fetchHandler = toFetchHandler(handler)
  return (input, init) => fetchHandler(new Request(input, init))
}

const client = createClient({
  baseURL,
  routes,
  fetch: createLocalFetch(handler),
})

The wrapper matters because generated clients call fetch(input, init), while a Rouzer fetch handler receives one Request.

Use ctx.host for host data

Host-provided values now live under ctx.host.

const ip = ctx.host.ip
const runtimeName = ctx.host.runtime?.name

If old code read adapter-specific fields directly from the context, move those values behind the host option when creating the fetch handler.

const fetch = toFetchHandler(router, {
  host: request => ({
    ip: request.headers.get('x-forwarded-for') ?? undefined,
    runtime: { name: 'custom' },
  }),
})

Rename runtime filters

If code used the old platform filter helper from Rouzer's root exports, use filterRuntime.

// v5
import { filterPlatform } from 'rouzer'

const middleware = chain().use(filterPlatform('node'))
// v6
import { filterRuntime } from 'rouzer'

const middleware = chain().use(filterRuntime<{ name: 'node' }>('node'))

filterRuntime(name) checks ctx.host.runtime?.name and narrows the downstream runtime type.

Update custom contexts

If tests or custom adapters constructed request contexts directly, use createContext with a Web Request and optional host data.

import { createContext } from 'rouzer'

const context = createContext({
  request: new Request('https://example.test/api/health'),
  host: {
    ip: '127.0.0.1',
    runtime: { name: 'test' },
    env: name => process.env[name],
    waitUntil: promise => {
      void promise
    },
  },
})

const response = await router(context)

Update pass-through assumptions

ctx.passThrough() is middleware-chain control flow. It skips the rest of the current chain and lets the parent chain continue. In a final fetch handler, an unresolved request becomes Rouzer's default 404 Not Found response.

Use ctx.passThrough() for optional middleware branches and runtime filters. Return a Response when you want to stop the request deliberately.

What stays the same

These Rouzer APIs keep the same shape:

  • rouzer/http resources and actions
  • createRouter().use(routes, handlers) route registration
  • middleware added with .use(middleware)
  • generated clients from createClient({ baseURL, routes })
  • flat client action input objects
  • $type<T>(), $error<T>(), and response maps
  • response plugins such as ndjson.routerPlugin and ndjson.clientPlugin
  • http.rawBody() request bodies

Checklist

  1. Remove Hattip packages that were only used for Rouzer serving or tests.
  2. Mount routers with toFetchHandler(router).
  3. Replace Hattip handler/context types with Rouzer RequestHandler and RequestContext.
  4. Replace test adapters with a local fetch wrapper.
  5. Move host metadata into the host option and read it from ctx.host.
  6. Replace platform filters with filterRuntime.
  7. Keep route trees, handler maps, clients, responses, and plugins unchanged unless your application code was also changing them.