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/httpresources and actionscreateRouter().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.routerPluginandndjson.clientPlugin http.rawBody()request bodies
Checklist
- Remove Hattip packages that were only used for Rouzer serving or tests.
- Mount routers with
toFetchHandler(router). - Replace Hattip handler/context types with Rouzer
RequestHandlerandRequestContext. - Replace test adapters with a local fetch wrapper.
- Move host metadata into the
hostoption and read it fromctx.host. - Replace platform filters with
filterRuntime. - Keep route trees, handler maps, clients, responses, and plugins unchanged unless your application code was also changing them.