Skip to main content

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.

src/index.ts
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

OptionTypeDefaultDescription
corsboolean or hono/cors optionsofftrue allows any origin (origin: '*'). An object is passed to Hono's cors().
requestIdboolean or {trustIncomingHeader?: boolean}trueSets c.var.requestId and the X-Request-Id response header. false disables it.
timingbooleanisDevHono timing() middleware (Server-Timing header). Off in production unless set to true, because it exposes handler durations.
healthCheckbooleanoffRegisters GET /health returning {status: 'healthy', timestamp}.
errorsErrorHandlerConfigproduction-safe defaultsOptions for the global error handler. See Error handling.
loggerLoggerroot loggerBase logger for request-scoped loggers and request log lines.
requestLoggingboolean or {level?, skip?}true (level info)One log line per request. false removes the line; the request-scoped logger is still set.
middlewareMiddlewareHandler[][]App-wide middleware. Runs after the built-ins and /health, before every module route and the docs routes.
pathstring'/'Base path for module routes.
modulesApiModule[][]API modules from defineModule. See Modules and versioning.
openApi(app: Hono<E>) => OpenAPIRouterrequiredWraps 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:

#StepControlled by
1Global error handler (app.onError)errors
2Non-Error throws: throw 'x' or throw {} is rethrown as an Error (original value as cause), so it still gets the envelopealways on
3c.respond* helpers (contextHelpers)always on
4Request idrequestId
5Server-Timingtiming
6CORScors
7Request-scoped logger and request log linelogger, requestLogging
8GET /healthhealthCheck
9Your app-wide middlewaremiddleware
10OpenAPI document and docs UI routes, then module routes (each with its module's middleware), all registered through the router returned by openApiopenApi, 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.

Don't add auth with app.use() after createApp

Hono 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:

  1. A valid incoming X-Request-Id header, as accepted by Hono's request-id middleware (at most 255 characters of [A-Za-z0-9_=-]).
  2. The cf-ray header, if it matches ^[A-Za-z0-9-]{1,64}$.
  3. 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:

src/app-env.ts
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>;
src/index.ts
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:

src/index.ts
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 everywhere

The 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.