App builder and middleware order
createApp(config) returns a configured Hono app. It validates module route keys, installs the built-in middleware, your middleware, the module routes and the OpenAPI router, in that order.
import {fromHono} from 'chanfana';
import {createApp} from '@ltv/cwb';
import {users} from './modules/users';
import {requireAuth} from './middleware/require-auth';
const app = createApp({
path: '/api',
cors: {origin: ['https://app.example.com'], credentials: true},
healthCheck: true,
requestLogging: {skip: (c) => c.req.path === '/health'},
errors: {context: {includeClientInfo: false}},
middleware: [requireAuth],
modules: [users],
openApi: (hono) => fromHono(hono, {docs_url: '/docs'}),
});
export default app;
Options
| Option | Type | Default | Description |
|---|---|---|---|
cors | boolean or hono/cors options | off | true allows any origin (origin: '*'). An object is passed to Hono's cors(). |
requestId | boolean or {trustIncomingHeader?: boolean} | true | Sets c.var.requestId and the X-Request-Id response header. false disables it. |
timing | boolean | isDev | Hono timing() middleware (Server-Timing header). Off in production unless set to true, because it exposes handler durations. |
healthCheck | boolean | off | Registers GET /health returning {status: 'healthy', timestamp}. |
errors | ErrorHandlerConfig | production-safe defaults | Options for the global error handler. See Error handling. |
logger | Logger | root logger | Base logger for request-scoped loggers and request log lines. |
requestLogging | boolean or {level?, skip?} | true (level info) | One log line per request. false removes the line; the request-scoped logger is still set. |
middleware | MiddlewareHandler[] | [] | App-wide middleware. Runs after the built-ins and /health, before every module route and the docs routes. |
path | string | '/' | Base path for module routes. |
modules | ApiModule[] | [] | API modules from defineModule. See Modules and versioning. |
openApi | (app: Hono<E>) => OpenAPIRouter | required | Wraps the app in a chanfana router and returns it, for example (app) => fromHono(app). |
Types for the nested options are exported: RequestIdConfig, RequestIdOptions, RequestLoggingConfig, RequestLoggingOptions and RequestLogLevel. The full shape is in the configuration reference.
Middleware order
Every request passes through these steps in order:
| # | Step | Controlled by |
|---|---|---|
| 1 | Global error handler (app.onError) | errors |
| 2 | Non-Error throws: throw 'x' or throw {} is rethrown as an Error (original value as cause), so it still gets the envelope | always on |
| 3 | c.respond* helpers (contextHelpers) | always on |
| 4 | Request id | requestId |
| 5 | Server-Timing | timing |
| 6 | CORS | cors |
| 7 | Request-scoped logger and request log line | logger, requestLogging |
| 8 | GET /health | healthCheck |
| 9 | Your app-wide middleware | middleware |
| 10 | OpenAPI document and docs UI routes, then module routes (each with its module's middleware), all registered through the router returned by openApi | openApi, modules, path |
Requests that match no route get the JSON NOT_FOUND envelope (app.notFound) instead of Hono's plain-text 404.
Before any of this, createApp validates every route key and throws on malformed keys, unsupported methods or duplicate routes, so a bad configuration fails at startup instead of on the first request.
app.use() after createAppHono runs handlers in registration order. createApp registers the module routes before it returns, so middleware added later with app.use() runs after the routes and never guards them. Use createApp({middleware: [...]}) for app-wide middleware, or defineModule({middleware: [...]}) for one module.
Because /health is registered before app-wide middleware, it stays reachable without credentials. Everything registered after step 9, including the chanfana docs routes and unknown routes, goes through your middleware.
Request ids
With the default requestId: true, the id is chosen in this order:
- A valid incoming
X-Request-Idheader, as accepted by Hono's request-id middleware (at most 255 characters of[A-Za-z0-9_=-]). - The
cf-rayheader, if it matches^[A-Za-z0-9-]{1,64}$. - A generated CUID2 (
createId()).
The deprecated, client-spoofable cf-request-id header is never used.
// Untrusted callers must not choose their own ids: always generate one
createApp({requestId: {trustIncomingHeader: false} /* ... */});
// Disable the middleware (no X-Request-Id header, no meta.requestId)
createApp({requestId: false /* ... */});
The id appears in the X-Request-Id response header, in meta.requestId of every envelope, and on every line logged through the request-scoped logger.
Request logging
The request logging step sets a child logger bound to requestId (read it with getRequestLogger(c)) and, after the response, logs one line with method, path, status, duration, cfRay and cfColo.
import {createApp, createLogger} from '@ltv/cwb';
createApp({
// base logger: every request line and request logger carries {app: 'billing'}
logger: createLogger({app: 'billing'}),
// debug level, and no line for health probes
requestLogging: {
level: 'debug',
skip: (c) => c.req.path === '/health' && c.res.status === 200,
},
// ...
});
skip(c) runs after the handler, so c.res.status is available. If skip throws, the error is logged and the request line is written anyway. See Logging.
CORS
cors: true is shorthand for cors({origin: '*'}). For credentials or an allow-list, pass Hono's options object:
createApp({
cors: {
origin: ['https://app.example.com', 'https://admin.example.com'],
allowHeaders: ['Content-Type', 'Authorization'],
credentials: true,
},
// ...
});
Typing the Hono env
createApp is generic over Hono's Env. Pass your bindings and variables to get typed c.env and c.get() in the openApi callback and in your own middleware:
import type {AppEnv, BaseContextVariables} from '@ltv/cwb';
export interface AppVariables extends BaseContextVariables {
orgId?: string; // your own variables
}
// Env comes from `wrangler types` (worker-configuration.d.ts)
export type AppHonoEnv = AppEnv<AppVariables, Env>;
const app = createApp<AppHonoEnv>({
modules: [users],
openApi: (hono) => fromHono(hono),
});
BaseContextVariables declares user, tenant, tenantId, requestId and traceId. It has no index signature, so a typo such as c.get('tenatId') fails to compile. Extend the interface to add your own keys.
Exporting the Worker
The returned app is a normal Hono app. Export it directly, or combine app.fetch with other handlers:
export default {
fetch: app.fetch,
async scheduled(controller, env, ctx) {
// cron work
},
} satisfies ExportedHandler<Env>;
Using parts without createApp
On a plain new Hono() app, install the pieces yourself:
import {Hono} from 'hono';
import {contextHelpers, errorHandler} from '@ltv/cwb';
const app = new Hono();
app.onError(errorHandler); // same handler as createApp, default options
app.use('*', contextHelpers); // enables c.respond* at runtime
c.respond type-checks everywhereThe c.respond* methods are added to Hono's Context type for every app, but they exist at runtime only on createApp apps or apps that use contextHelpers. On a plain Hono app without the middleware, c.respond is undefined.