v5.5.0 ยท npm package ยท open source

Hono with
NestJS-style decorators.

Controllers, DI, modules, guards, SSE, WebSocket, channels, caching, events, scheduling, resilience, OpenAPI, typed client, and testing โ€” all declarative. Install one package. No boilerplate.

View on GitHub โ†—Quick start โ†’
$bun add hono-forge hono zod
Hono 4.0+Bun 1.2+Node 18+TypeScript 5.0+Zod 4SSEWebSocketRedisOpenAPI 3.1
90k+
req / sec
60+
decorators
...
GitHub stars
...
npm downloads
what's inside

Everything you need.
Nothing you don't.

One package. No boilerplate. Focus on business logic.

๐ŸŽจ
Decorator-Based Routing
NestJS-inspired decorators for clean, declarative controllers. Metadata-driven routing with full TypeScript inference.
@Controller@Get @Post@Put @Delete @All
๐Ÿ”
Auth & Authorization
Pluggable guard executor with role-based and permission-based access control. Throws 401/403 automatically.
@RequireAuth@RequireRole@Public
โœ…
Zod Validation โ†’ Spec
@Validate gives one source of truth: schemas validate requests AND generate the OpenAPI spec โ€” docs can't drift from validation.
@ValidateBody(c)Query(c)Params(c)
๐Ÿ“ก
SSE & WebSocket
First-class streaming with @Sse and @WebSocket decorators. Built-in keepalive and pluggable upgrader for any Hono runtime.
@Sse(keepAliveMs)@WebSocketSSEStreamingApi
๐Ÿ”
Pub/Sub Channels
Broadcast events to SSE and WebSocket clients. In-memory by default, swap to Redis for multi-instance deployments.
channels.publishSseChannelClientRedisChannelAdapter
๐Ÿ›ก๏ธ
Rate Limiting
Per-route rate limiting with X-RateLimit-* headers out of the box. In-memory limiter by default, pluggable for any backend.
@RateLimitX-RateLimit-*custom keyGen
๐Ÿ“Š
Request Logging
Pluggable logger with IP extraction, device detection, and duration tracking. Read IP/device anywhere in handlers.
Ip(c)Device(c)UserAgent(c)
๐Ÿ’‰
Dependency Injection
Lightweight DI container with singleton, transient, and request-scoped lifetimes. Lifecycle hooks, circular dependency detection, and auto-resolution.
@Injectable@Singleton@RequestScopedOnInit/OnDestroy
๐Ÿ“–
OpenAPI 3.1 + Scalar
Auto-generate a full OpenAPI spec from your decorators โ€” including request bodies and query params. Serve interactive Scalar docs with one line.
OpenAPIGenerator@ApiDoc@ApiBody@ApiQuery
โšก
Interceptors
Cross-cutting concerns without middleware clutter. Retry, timeout, transform, cache, and metrics decorators.
@Retry@Timeout@Transform@Cache@TrackMetrics
๐Ÿงฉ
Modules
Compose your app from @Module blocks. buildModule() traverses imports recursively and fail-fast validates DI at boot.
@ModulebuildModuledescribe()
โ™ป๏ธ
Resilience
Production-grade request safety: idempotency keys, concurrent dedupe, and circuit breakers for external calls.
@Idempotent@SingleFlight@CircuitBreaker
๐Ÿ“ฎ
Event Bus & Scheduler
In-process pub/sub for decoupled services, plus periodic jobs. Listeners resolve through the DI container.
@OnEventevents.emit@Interval
๐Ÿงช
Testing & Typed Client
createTestingModule() mocks providers cleanly. generateClientTypes() emits a fully-typed fetch client from your controllers.
createTestingModulecreateClientgenerateClientTypes
๐Ÿ›Ÿ
Ops Ready
Health checks, graceful shutdown, route table printing, and pluggable logging โ€” the boring parts are done for you.
mountHealthgracefulShutdownprintRoutesLOGGER
๐ŸŒ
Built-in Middleware
First-class decorators for the most common Hono middleware. No boilerplate โ€” just stack and go.
@Cors@Compress@SecureHeaders@PrettyJson
๐Ÿšจ
Structured Error Handling
HttpException with static factories for every HTTP error. Auto-serialized to consistent JSON. Optional stack trace exposure for development.
HttpException.badRequest().notFound()exposeStack
๐Ÿ”ญ
Observability
Every request gets a trace ID from X-Request-ID or auto-generated UUID. Access it anywhere โ€” services, repos, loggers โ€” without passing it explicitly.
getTraceId()onRequestStartX-Request-ID
quick start

From zero to API
in minutes.

Install one package, decorate your controllers, mount the routes.

1. Install
terminal
$ bun add hono-forge hono zod
2. No setup needed
tsconfig.json
{
  "compilerOptions": {
    "target": "ESNext"
  }
}
3. Write your controller
controller.ts
import { Hono } from 'hono';
import type { Context } from 'hono';
import { 
  Controller, Get, Post, Patch, Delete,
  Body, Param, Public, RequireAuth,
  Injectable, HonoRouteBuilder,
} from 'hono-forge';
import { z } from 'zod';

const CreateUserSchema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
});
const UpdateUserSchema = CreateUserSchema.partial();

@Injectable()
class UserService {
  getAll() { return [{ id: 1, name: 'Alice' }]; }
  create(data: any) { return { id: 2, ...data }; }
}

@Controller('/users')
@Injectable([UserService])
class UserController {
  constructor(private userService: UserService) {}

  @Get() @Public()
  list() { return this.userService.getAll(); }

  @Post() @RequireAuth()
  async create(c: Context) {
    const body = await Body(c, CreateUserSchema);
    return this.userService.create(body);
  }

  @Patch('/:id') @RequireAuth()
  async update(c: Context) {
    const id   = Param(c, 'id');
    const body = await Body(c, UpdateUserSchema);
    return this.userService.update(id, body);
  }

  @Delete('/:id') @RequireAuth()
  remove(c: Context) {
    return this.userService.delete(Param(c, 'id'));
  }
}

const app = new Hono();
app.route('/', HonoRouteBuilder.build(UserController));
export default app;
reference

All decorators.

Every decorator and API available in hono-forge, organized by category. Hover any item to see a usage example.

routing
@Controller(path?, opts?)hover
Registers a class as a controller. Options: platform and version โ€” { version: 'v2' } prefixes all routes with /v2.
@Get(path?)hover
Maps method to HTTP GET.
@Post(path?)hover
Maps method to HTTP POST.
@Put(path?)hover
Maps method to HTTP PUT.
@Delete(path?)hover
Maps method to HTTP DELETE.
@Patch(path?)hover
Maps method to HTTP PATCH.
@Head(path?)hover
Registered as GET; Hono handles HEAD requests via fallback.
@Options(path?)hover
Maps method to HTTP OPTIONS.
@All(path?)hover
Matches all HTTP methods on the given path.
@Status(code)hover
Sets the default HTTP status for the response โ€” e.g. 201 for POST, without calling c.json(data, 201). A returned Response still wins.
@NoContent()hover
Returns 204 with an empty body regardless of the handler return value.
@Header(name, value)hover
Sets a static response header. Repeatable โ€” multiple @Header decorators merge. Bypassed by a raw new Response() return.
@CacheControl(opts)hover
Sets the Cache-Control header โ€” typed options object (maxAge, public/private, noStore, staleWhileRevalidate, immutable) or a raw directive string.
@ETag(opts?)hover
Emits an ETag computed from the JSON body (SHA-256, truncated). Weak W/"โ€ฆ" by default; { weak: false } for strong. Matching If-None-Match short-circuits to 304.
@Redirect(location, status?)hover
Always redirects โ€” the handler is never invoked. Status defaults to 302 (301/302/303/307/308).
modules & bootstrap
@Module({ controllers, providers, imports })hover
Groups controllers and providers into a composable unit. imports are traversed recursively and deduplicated. Modules organize wiring โ€” they do not create isolated DI scopes.
HonoRouteBuilder.buildModule(module)hover
Builds routes from a @Module class. providers are resolved eagerly so missing DI tokens fail at boot, not at request time.
HonoRouteBuilder.build(ctrl, platform?, opts?)hover
Builds a single controller into a Hono sub-app. Filter by platform, or { excludePrivate: true } to skip @Private routes.
createApp(module, opts?)hover
One-call async bootstrap: buildModule + optional /health probes + /docs OpenAPI + /metrics + CORS + printRoutes + gracefulShutdown, then awaits container.boot() so onInit() hooks run before the app is returned. Pass boot: false to skip (tests managing lifecycle manually), strictDi: true to forbid bare `new` on @Injectable classes. Every option optional.
HonoRouteBuilder.describe(target)hover
Returns route metadata (method, path, guards, middlewares, cache, rateLimit, sse, websocket, isPublic, isPrivate) for a controller or module โ€” without instantiating anything.
HonoRouteBuilder.printRoutes(target)hover
Prints a route table of every route โ€” method, path, handler, guards, flags. Pure metadata read; great during dev startup.
HonoRouteBuilder.moduleControllers(module)hover
Returns every controller class reachable from a @Module (imports traversed, deduplicated). Useful for OpenAPI / client codegen.
discoverControllers(glob)hover
Bun-only runtime filesystem scan โ€” imports matching files and returns every @Controller class found.
fromModules(modules)hover
Bundler-friendly discovery โ€” pass the result of import.meta.glob({ eager: true }) and get every @Controller class back. Other exports are ignored.
dependency injection
@Injectable(tokens?)hover
Registers a class in the DI container. Pass explicit dependency tokens โ€” array index maps to constructor parameter position. Symbol/string tokens work for external objects.
@Singleton()hover
Returns the same instance on every resolve().
@RequestScoped()hover
Fresh instance per request, destroyed automatically when the request ends. Shared within the same request. Injecting one into a singleton wires a lazy proxy resolved per request.
@Stateless()hover
No-op marker for @Singleton classes that hold no mutable per-request state โ€” documents intent and can be enforced by tooling.
OnInit / OnDestroyhover
Interfaces for lifecycle hooks โ€” onInit runs when the provider is resolved/booted, onDestroy on container.shutdown() in reverse registration order.
@OnBootstrap()hover
Method-level startup hook โ€” runs once after buildModule() mounts every route and boots singletons. Sequential and awaited; @RequestScoped classes are rejected (doctor flags it too).
container.registerInstance(token, value)hover
Registers a pre-built external object (Drizzle, Prisma, Redis, etc.) under a symbol token. Recommended name for external deps.
container.registerSingleton / registerFactoryhover
Manual registration โ€” bind a class token to an instance, or a factory invoked per resolve.
container.boot()hover
Calls onInit() on all registered singletons that implement OnInit. createApp() already awaits it โ€” call yourself only with manual buildModule()/build() wiring.
container.shutdown()hover
Calls onDestroy() on all singletons in reverse registration order. Call in SIGTERM/SIGINT handler.
strict DI โ€” @Injectable(deps, { strict }) / strictDihover
Opt-in guard against leaving the container: bare `new` on a strict @Injectable class throws 'DI bypass'. Enable per class via @Injectable(deps, { strict: true }) or globally via createApp(AppModule, { strictDi: true }) / container.strictDi = true. container.resolve(), constructor injection, and registerFactory() internals still construct normally.
container.construct(cls, ...args)hover
The official escape hatch under strict DI โ€” creates an unmanaged instance with caller-provided args (no injection, no singleton caching, no lifecycle hooks). For tests and scripts.
container.runInScope(fn)hover
Runs fn in a new request scope. onDestroy is called on all scoped instances in the finally block.
container.describe() / printGraph()hover
Introspects the dependency graph without resolving โ€” scope, deps, registration kind, missing deps. printGraph renders a human-readable table.
DI error classeshover
DependencyResolutionError โ€” thrown by resolve() when a token isn't in providers/registrations (note: a concrete class decorated with @Injectable() self-registers and never throws this). CircularDependencyError โ€” thrown when the resolution stack revisits a token mid-construction (A โ†’ B โ†’ A).
metadata introspectionhover
Read decorator metadata for plugin/tooling authors โ€” getClassMetadata(class, key) and getMethodMetadata(target, method, key) read values stored under METADATA_KEYS.* symbols; hasDecorator(target, method, key) is a boolean check. Lower-level getClassMeta/getMethodMeta read from a class's Symbol.metadata record directly.
context helpers
Body(c, schema?)hover
Reads + validates request body. With @Validate set, returns the pre-validated value (transforms/defaults applied); pass a schema for ad-hoc validation.
Param(c, name)hover
Reads a route path parameter by name. Omit name to get all params as raw strings.
Params<T>(c)hover
All route params as an object โ€” the @Validate-parsed typed version when set, raw strings otherwise.
Query(c, schema?)hover
Reads query params. With @Validate set, returns the pre-validated object; pass a Zod schema for ad-hoc validation and coercion.
Headers(c, name?)hover
Reads a request header. Omit name to get all headers as an object.
User<T>(c)hover
Returns the authenticated user object set on the context by the guard or @JwtAuth โ€” shorthand for c.get('user').
Req(c)hover
Returns the Hono HonoRequest object.
Ctx(c)hover
Returns the full Hono Context c โ€” for redirect, set-cookie, etc. (Res() is a deprecated alias โ€” use Ctx.)
Ip(c)hover
Returns the real client IP โ€” CF-Connecting-IP โ†’ X-Real-IP โ†’ X-Forwarded-For โ†’ 'unknown'. Proxy headers are trusted unconditionally.
Device(c)hover
Returns detected device type: 'mobile' | 'tablet' | 'desktop' | 'bot'.
UserAgent(c)hover
Returns the raw User-Agent header string.
Cookie(c, name)hover
Returns a single cookie value by name.
Cookies(c)hover
Returns all cookies as Record<string, string>.
SetCookie(c, name, value, opts?)hover
Sets a cookie โ€” options match Hono setCookie: httpOnly, sameSite, maxAge, secure, path, domain.
DeleteCookie(c, name, opts?)hover
Deletes a cookie by name.
UploadedFile(c, field)hover
Returns a single File from multipart form data by field name. Returns File | null.
UploadedFiles(c, field?)hover
Returns all File objects for a field. Omit field to collect every file in the form.
FormBody(c)hover
Returns the raw FormData object.
ValidatedBody / ValidatedQuery / ValidatedParamshover
Schema-first helpers โ€” return type infers as z.infer<typeof schema>, no manual generics. Cache-first: if @Validate already parsed the request, the cached value is returned; otherwise the helper parses through your schema itself (works on undecorated routes). Without a schema they degrade to the raw Body/Query/Params helpers.
ValidatedParam(c, name, schema)hover
Reads a route param and validates it through a Zod schema. Invalid input throws ZodError โ†’ 400 VALIDATION_ERROR.
getContext()hover
Returns the active Hono Context from anywhere in the request call chain โ€” services, repos โ€” without prop drilling. undefined outside a request.
getRequestContext()hover
Returns the full request scope context ({ traceId, hono, memoCache, diScope }) โ€” the lower-level API behind getContext(). runInRequestContext(ctx, fn) runs code inside a context; createRequestContext(traceId, hono?) builds one โ€” HonoRouteBuilder calls these once per request, so you only need them when invoking handlers outside the route builder.
extractIp / detectDevice / extractUserAgent / extractRequestContexthover
The raw extractor utilities behind Ip/Device/UserAgent โ€” usable outside handlers (middleware, loggers). extractIp honors CF-Connecting-IP โ†’ X-Real-IP โ†’ X-Forwarded-For; detectDevice classifies a UA string into 'mobile' | 'tablet' | 'desktop' | 'bot'; extractRequestContext(c) returns all three in one call ({ ip, device, userAgent }).
validation & serialization
@Validate({ body?, query?, params? })hover
Validates body/query/params before the handler runs โ€” invalid input returns 400 VALIDATION_ERROR with Zod issues. The same schemas are emitted into the OpenAPI spec, so no @ApiBody duplication.
@ValidateResult(schema)hover
Validates the return value against a Zod schema. Throws ZodError if the result doesn't match โ€” enforce response contracts at service boundaries.
@Serialize(schema)hover
Runs the handler's return through a Zod schema before sending โ€” strip sensitive fields with .omit()/.pick() or coerce output shape. Validation errors become 400.
@Transform(fn)hover
Applies a transform function to the return value before sending.
defineSchemas(select, insert, opts?)hover
Generates a consistent { select, insert, update } schema set from any two Zod schemas โ€” works with drizzle-zod, zod-prisma, or hand-written schemas. update defaults to insert.partial().
auth & guards
guardExecutor confighover
The pluggable auth hook โ€” configure once via HonoRouteBuilder.configure. Throw HttpException for precise status codes; "Unauthorized" in message โ†’ 401, "Forbidden" โ†’ 403, return false โ†’ 403.
@RequireAuth()hover
Attaches AuthGuard โ€” your guardExecutor validates the token and sets the user via c.set('user'). Read it back with User(c).
@RequireRole(...roles)hover
Requires user to have ONE of the specified roles.
@RequireAllRoles(...roles)hover
Requires user to have ALL specified roles.
@RequirePermission(...perms)hover
Requires ALL listed permissions.
@RequireAnyPermission(...perms)hover
Requires at least ONE of the listed permissions.
@Public()hover
Marks route as public, bypassing all auth guards.
@Private()hover
Marks route as internal-only โ€” still registered, but excluded when build() is called with { excludePrivate: true }.
@JwtAuth(opts)hover
Turnkey JWT auth โ€” verifies the Bearer token with hono/jwt (HS256 default; alg, cookie options available) and stores the payload for User(c). Reflected in OpenAPI as bearerAuth + 401/403 responses, and the flow generator detects it the same way โ€” protected routes get Authorization: Bearer {{token}} (or ?token={{token}} on generated WS nodes). Invalid/missing token โ†’ 401.
@UseGuards(...guards)hover
Attaches custom guard classes by constructor โ€” the executor receives them like built-in guards.
middleware
execution orderhover
The request pipeline runs class-level @Middleware first, then method-level, then rate limiting (@RateLimit/@Concurrency), guards (@JwtAuth/@RequireRole/โ€ฆ), and validation (@Validate). @Idempotent wraps the handler โ€” replay/dedupe happens around execution. @Throttle sits inside the method body (it throws when the handler is invoked too soon). On the way out, handler output passes @Serialize/@Transform, then interceptors, then the response.
@Middleware(...fns)hover
Attaches Hono middleware at class level (all routes) or method level (one route). Multiple middleware run in order.
@Use(...fns)hover
Alias for @Middleware โ€” familiar for NestJS/Express users.
MiddlewareClasshover
Interface for reusable stateful middleware โ€” implement use(c, next) and pass the class to @Use/@Middleware.
@Cors(opts?)hover
Applies CORS headers. Options match hono/cors โ€” origin, allowMethods, maxAge, etc. Class or method level.
@Compress(opts?)hover
Applies gzip/deflate response compression. Works at class or method level.
@SecureHeaders(opts?)hover
Adds security headers: CSP, HSTS, X-Frame-Options, X-Content-Type-Options, etc.
@PrettyJson(opts?)hover
Pretty-prints JSON responses when ?pretty query param is present.
interceptors & caching
@Cache(opts)hover
Caches the handler return value in the configured CacheAdapter โ€” in-memory by default, swappable for Redis/Postgres. Keyed by key (default: method name) + request path + query. ttl (ms) required; adapter picks a named cacheAdapters entry. Cached hits skip the handler entirely.
@CacheInvalidate(...patterns)hover
Deletes cache entries whose key starts with each pattern after the handler succeeds.
cacheAdapter confighover
Swap the in-memory cache (InMemoryCacheAdapter, also exposed as defaultCacheAdapter) for a durable backend โ€” ship RedisCacheAdapter (ioredis-shaped client) or PostgresCacheAdapter (driver-free sql fn + one table, strongest for @Idempotent). Named cacheAdapters enable per-decorator picks: @Cache({ adapter: 'redis' }) vs @Idempotent({ adapter: 'pg' }).
@Memoize(opts?)hover
Caches the return value in memory keyed by serialized arguments. scope: 'global' (default, shared across requests) or 'request' (isolated per request, via the request context). Optional ttl. Calling a request-scoped @Memoize method outside a request falls back to the global cache โ€” wrap the call in runWithMemoScope(fn) to give it a scope.
@TrackMetrics(opts)hover
Records method duration into the shared Prometheus registry as hono_forge_method_duration_seconds{method,status}. Options: name.
@Audit(opts)hover
Logs an audit entry (action, userId, timestamp, method name) through the registered LOGGER before the method runs โ€” falls back to ConsoleLogger. Options: action.
@Transaction(exec?)hover
Wraps the method in this.db.transaction() โ€” Drizzle/Knex/TypeORM compatible by default. Pass a custom TransactionExecutor for Prisma or Kysely. The tx propagates via AsyncLocalStorage: nested repos call useTransaction() to join it.
rate limiting & resilience
@RateLimit(opts)hover
Rate-limits the route โ€” built-in in-memory fixed-window limiter works out of the box, keyed by client IP (or keyGenerator). Emits X-RateLimit-Limit/Remaining/Reset headers; exceeding max returns 429 + Retry-After. Options: max, windowMs, message, keyGenerator.
redisRateLimiter(client)hover
Distributed @RateLimit via Redis (INCR + PEXPIRE fixed window) โ€” inMemoryRateLimiter is the default factory and is per-process, so multi-instance deploys need this. Client is injected (ioredis-compatible, or anything exposing incr/pexpire/pttl). Factories receive RateLimiterFactoryOptions: { max, windowMs, keyPrefix, keyGenerator?, message? }.
@Throttle(ms)hover
Prevents the method from being called more than once per ms window. Throws HttpException 429 (with retryAfterMs in meta + Retry-After header). Keyed on the instance โ€” on a singleton it applies globally across all users.
@Concurrency(n)hover
Per-route bulkhead โ€” caps simultaneous executions; extra requests get a fast 503 instead of queueing unbounded work. Slots release in finally โ€” exceptions never leak capacity.
@Idempotent(opts?)hover
Replays the stored response when a client retries with the same Idempotency-Key header โ€” including concurrent duplicates (handler runs once). Same key + different request body โ†’ 409 Conflict (Stripe-style body-hash check, on by default โ€” hash: false to opt out). scope namespaces keys per tenant/user. Only successful responses are stored; ttl defaults to 24h. Uses the configured CacheAdapter โ€” default in-memory does NOT survive restarts; use adapter: 'name' with a named cacheAdapters entry (e.g. PostgresCacheAdapter) in production.
@SingleFlight()hover
Coalesces concurrent identical requests (same method+path+query) โ€” the handler executes once and every caller gets a copy of that response. For client-driven retries on mutations, use @Idempotent instead.
@Retry(opts)hover
Retries the method on failure. Options: attempts, delay, backoff (linear | exponential).
@Timeout(ms)hover
Throws HttpException 504 GATEWAY_TIMEOUT if the method takes longer than ms milliseconds โ€” also documented in the generated OpenAPI spec.
@CircuitBreaker(opts)hover
Stops calling a failing method after failureThreshold consecutive errors โ€” fast-fails with CircuitOpenError (503) until resetAfterMs passes, then probes with a half-open call. State is per instance+method.
error handling
HttpExceptionhover
Structured HTTP error with status, code, message, and optional meta payload โ€” auto-handled by the route builder. Factories: badRequest 400, unauthorized 401, forbidden 403, notFound 404, conflict 409, unprocessable 422, tooManyRequests 429, internal 500, serviceUnavailable 503.
@Catch(...errorTypes)hover
Marks a class as an exception filter โ€” no args = catch-all. The class implements catch(err, c) and is resolved through the DI container, so it can inject services.
@UseFilters(...filters)hover
Attaches exception filters at method or class level. Order: method filters โ†’ class filters โ†’ onError โ†’ default HttpException handling. A filter returning void falls through.
onError hookhover
Global hook called for every non-validation error (including HttpException). Return a Response to override; return void to fall through to default handling. ZodError always returns 400 and bypasses this hook.
errorFormat: 'problem'hover
Opt-in RFC 9457 application/problem+json error shape instead of the default envelope. Validation failures become a 400 problem with Zod issues under errors; headers set before the throw (e.g. Retry-After) are preserved. problemResponse(c, err, exposeStack) renders the same shape โ€” reuse it inside a custom onError; returns null for non-HttpException/non-Zod errors.
exposeStack optionhover
Controls stack trace exposure in HttpException responses. Values: false (default, production-safe) | true | "development" (NODE_ENV !== 'production').
real-time: sse, websocket & channels
@Sse(path?, opts?)hover
Registers a GET endpoint for SSE. Handler receives (c: Context, stream: SSEStreamingApi). Pass { keepAliveMs } to send :keepalive comments on an interval โ€” keeps proxies from closing idle streams.
@WebSocket(path?)hover
Registers a WebSocket upgrade endpoint โ€” configure a platform upgrader once (e.g. hono/bun). Handler receives (c: Context) and returns { onOpen, onMessage, onClose }.
@OnWsMessage(event)hover
Routes incoming WS frames to a sibling method by event name โ€” JSON envelope { "event", "data" } dispatches to (data, ws, rawEvent). Non-envelope frames and unmatched events fall through to onMessage.
@ChannelRoute(channel)hover
Bridges a pub/sub channel to an @Sse or @WebSocket route โ€” subscribes a client on connect, unsubscribes on disconnect, pushes every published event to the stream/socket. Static string or fn of Context.
@ChannelAuth(authorize)hover
Authorizes the resolved channel before the SSE/WS upgrade completes โ€” runs as middleware, receives (c, channel), returns false โ†’ 403. Pair with token verification at upgrade (e.g. hono/jwt verify of ?token= or Authorization) for the standard authenticated-channel recipe. Flow generation marks these routes too โ€” generated WS nodes get ?token={{token}} on the connection URL.
channels.subscribe / unsubscribe / publishhover
The shared channel registry API โ€” add/remove clients and broadcast event+data payloads to all subscribers of a channel.
SseChannelClient / WsChannelClienthover
Client adapters wrapping an SSE stream or WebSocket so the channel registry can push to them. WsChannelClient takes the ws instance + an id.
channels.use(adapter)hover
Swaps the channel adapter at startup โ€” default is InMemoryChannelAdapter. Use RedisChannelAdapter for multi-instance broadcasting.
events & scheduling
@OnEvent(event)hover
Subscribes a service method to an in-process event. Repeatable; supports wildcards like "user.*". Listeners resolve through the DI container per emit โ€” keep them singleton/transient, not @RequestScoped.
events.emit(event, data)hover
Emits an event to all matching @OnEvent listeners. Each listener is isolated โ€” a throwing one is logged via LOGGER, others still run; emit never throws.
startEventBus(...targets)hover
Scans classes or @Module trees for @OnEvent methods and subscribes them. Returns a teardown function โ€” wire it into gracefulShutdown.
@Interval(ms, opts?)hover
Runs a service method every ms milliseconds once startScheduler() activates it. Overlapping ticks are skipped; { immediate: true } runs once at start. Timers are unref'd.
@Cron(expr, opts?)hover
Schedules a method on a standard 5-field cron expression (min hour dom mon dow, local time). Supports *, */n, ranges, lists โ€” Vixie OR semantics when dom+dow are both restricted. Invalid expressions are logged and skipped.
startScheduler(...targets)hover
Activates all @Interval and @Cron methods on classes or @Module trees. Job errors are logged via LOGGER; the loop never crashes. Returns a teardown.
parseCron / nextCronTimehover
The exported cron primitives โ€” parse an expression once, compute the next fire time.
observability & config
getTraceId()hover
Returns the active trace/correlation ID โ€” from the X-Request-ID header or a generated UUID, echoed back on the response. Callable anywhere in the call chain.
runWithTraceId(id, fn)hover
Runs fn within a trace context โ€” for code outside the route builder (cron, workers).
onRequestStart hookhover
Called before middleware and guards on every request. Receives { method, path, traceId, ip, userAgent } โ€” use it to start an OpenTelemetry span.
requestLogger hookhover
Pluggable per-request logger โ€” receives { method, path, ip, device, userAgent, statusCode, durationMs, userId? } after each response.
registerLogger(logger) / LOGGERhover
Binds a pino/winston-compatible logger ({ info(data, msg?) }) to the LOGGER DI token โ€” used by @Audit, the scheduler, and the event bus. Injectable anywhere. getLogger() resolves the registered logger with a ConsoleLogger fallback when nothing is bound.
registerConfig(schema)hover
Parses process.env through a Zod schema once at boot โ€” fails fast listing every invalid field. Returns the typed config and binds it to the CONFIG token.
CONFIG token / getConfig()hover
Access the registered config โ€” inject via @Injectable([CONFIG]) or call getConfig<T>() anywhere outside DI.
mountMetrics(app, opts?)hover
Mounts GET /metrics in Prometheus text format โ€” http_requests_total + http_request_duration_seconds histogram labeled by method/route/status. Mount BEFORE building routes; the metrics path is never self-counted.
getMetricsRegistry()hover
Returns the shared MetricsRegistry โ€” read counters/histograms programmatically (custom exporters, tests, in-process dashboards) without scraping /metrics.
openapi documentation
OpenAPIGenerator.generate()hover
Generates a full OpenAPI 3.1 spec from your controller classes.
OpenAPIGenerator.mount()hover
Serves /openapi.json and Scalar UI at /docs. Accepts a generated spec OR a controller array โ€” generates and mounts in one call.
@ApiTags(...tags)hover
Groups controller endpoints under named tags in the OpenAPI spec.
@ApiDoc(opts)hover
Documents an endpoint. Options: summary, description.
@ApiResponse(status, msg)hover
Describes a possible HTTP response for this endpoint โ€” repeatable. Pass { schema } to attach a response body schema.
@ApiBody(schema, opts?)hover
Documents the JSON request body in the spec. required is auto-inferred โ€” schemas with .optional()/.default() are optional.
@ApiQuery(name, schema, opts?)hover
Documents a query parameter in the spec. required is auto-inferred from the schema.
@ApiDeprecated(sunset?)hover
Marks the endpoint deprecated in the spec AND emits an RFC 9745 Deprecation response header. Pass a date to also emit a Sunset header.
@ApiSecurity(name)hover
Applies a named security requirement to a route โ€” use for schemes the auto-detection doesn't cover (apiKey, basic, OAuth2).
@ApiSecurityScheme(name, spec)hover
Declares a security scheme emitted under components.securitySchemes โ€” Scalar/Redoc pick it up for the Authorize button.
pagination
paginate(data, total, opts)hover
Wraps a data array and total count into a standard { data, meta } response โ€” meta carries page, limit, total, totalPages, hasNext, hasPrev. Return type is PaginatedResult<T> ({ data: T[]; meta: PaginationMeta }) โ€” PaginationMeta is the exported meta type.
PaginationQuerySchemahover
Zod schema for standard page/limit query params โ€” page defaults to 1, limit defaults to 20 (max 100). Use with @Validate or Query(c, schema).
paginatedSchema(itemSchema)hover
Wraps an item schema into a full paginated response schema โ€” for @ApiResponse or @ValidateResult.
ops, testing & tooling
mountHealth(app, opts?)hover
Mounts GET /health returning { status, uptime, checks } โ€” any failing check โ†’ 503. With probes: true also mounts /health/live (process alive, no deps) and /health/ready (dependency checks) for k8s liveness/readiness.
gracefulShutdown(opts?)hover
Hooks SIGINT/SIGTERM: onShutdown โ†’ drain in-flight HTTP requests โ†’ close channel subscribers (SSE/WS, WS 1001) โ†’ container.shutdown() โ†’ exit. Hard-exits after timeoutMs. Returns a detach function.
mountDrain(app)hover
Installs in-flight request tracking so gracefulShutdown can wait for running requests instead of cutting them off. Mount BEFORE building routes. createApp mounts it automatically when shutdown is enabled. getRequestDrain() returns the RequestDrain ({ active, isDraining, drain() }) for custom probes.
createTestingModule(meta)hover
Builds an isolated DI container for tests โ€” framework-agnostic (bun:test, vitest, node:test). overrideProvider().useValue/useClass/useFactory swaps dependencies; cleanup() restores the real container.
generateClientTypes(opts)hover
Emits an AppRoutes TypeScript interface from your controllers โ€” each output is the handler's Awaited<ReturnType>. Handlers returning Response produce Response in the map โ€” use plain-value returns for typed clients. Imports come from importPath by default; controllerSpecifiers (a Map of controller โ†’ module specifier) emits grouped per-file imports โ€” the CLI fills this in automatically from each controller's recorded source file.
createClient<AppRoutes>(baseUrl)hover
Typed fetch client keyed by "METHOD /path". Response types come from the generated interface. Throws ClientRequestError on non-2xx.
writeFlowFiles(dir, module, opts?)hover
Emits api-tester .flow.yaml files โ€” one per controller, CRUD-chained (POST extracts id โ†’ {{id}} downstream). @Validate/@ApiBody schemas become sample bodies (z.file() fields โ†’ form-data multipart rows; schema-less mutations get a visible empty raw body); @Validate({query})/@ApiQuery become query params; @Validate({params}) seeds the editable {{param}} variables; guard- or @JwtAuth-protected routes add Bearer placeholders โ€” WebSocket nodes get ?token={{token}} on the URL instead (sockets can't carry headers; @ChannelAuth routes and declared @ApiQuery params land there too, and the wsQuery option appends anything else). @Status/@ApiResponse(2xx) drive status assertions and @Serialize/@ApiResponse schemas seed exists assertions on required response fields. Deterministic ids โ†’ re-imports upsert. In-memory variants for your own writer: generateFlowFiles/generateFlowFile (returns [{filename, flow}]) and stringifyFlowFile (returns the YAML string).
db schema introspectionhover
Programmatic API behind hono-forge schema โ€” extractDbSchema(mod) reflects a module's exported Drizzle tables into a DbSchemaFile, introspectPostgresSchema(connStr, { schema? }) reads a live database via information_schema (needs 'pg' in the project), parsePrismaSchema(text) parses schema.prisma, and normalizeDbSchemaDoc(doc) canonicalizes hand-written/external docs. Write with writeDbSchema(file, schema) or serialize via stringifyDbSchema.
cli commands
hono-forge init <name>hover
Scaffolds a runnable starter project โ€” app.module.ts, an example controller/service, src/index.ts wiring health + OpenAPI docs + graceful shutdown, package.json, tsconfig.json (TC39 decorators configured), README.md, and .gitignore.
hono-forge schema <entry|conn-string>hover
Reflects a database schema into a portable .schema.yaml โ€” columns, vendor+portable types, notNull, PK/composite PK flags, the full FK graph, and dialect. Four sources, auto-detected or forced with --from: drizzle (.ts/.js exporting pgTable/mysqlTable/sqliteTable), prisma (schema.prisma model blocks โ€” no prisma CLI needed), pg (postgres://โ€ฆ entry or --connect <url> โ€” live information_schema introspection, any ORM, needs 'pg' installed), file (.yaml/.json โ€” normalizes hand-written/external docs). Options: --from, --connect, --out <file> (default ./db.schema.yaml), --json. api-tester's Database view imports the output.
hono-forge flows <entry>hover
Generates api-tester .flow.yaml files โ€” one per controller, CRUD-chained. Auth detection covers guards AND @JwtAuth middleware โ€” REST/SSE nodes get Bearer {{token}} headers, auth-protected and @ChannelAuth WS nodes get ?token={{token}} on the URL (sockets can't carry headers), and @ApiQuery params declared on SSE/WS handlers land on the URL as seeded {{var}} placeholders. @Serialize/@ValidateResult contracts seed exists assertions for required top-level fields ($[0].* for arrays), scoped with forStatus to the success status. Options: --out <dir> (default ./flows), --base-url <url> (default http://localhost:3000), --single (one file with a visual group per controller), --auth '<METHOD> <path>' + --auth-body/--auth-extract (prepend a login step so guarded flows actually run โ€” {{vars}} in the body become editable variables), --ws-query 'k=v,...' (extra params on every WS node URL, {{vars}} allowed), --export <name>.
hono-forge openapi <entry>hover
Writes the OpenAPI 3.1 spec to a JSON file without booting the server. Options: --out <file> (default ./openapi.json), --title <text> (default: entry file name), --version <ver> (default 1.0.0), --export <name>.
hono-forge routes <entry>hover
Prints the route table โ€” method / path / handler / guards / middleware / flags. Options: --json (machine-readable output), --export <name>.
hono-forge graph <entry>hover
Prints the DI dependency graph โ€” provider scopes, constructor deps, and missing-dep warnings. Options: --format text|mermaid (mermaid is embeddable in docs/PRs), --export <name>.
hono-forge doctor <entry>hover
Static wiring check โ€” exits 1 on issues so it works as a CI guard. Checks: missing DI deps, route collisions across controllers, invalid @Cron expressions, scheduled jobs on @RequestScoped classes, and @OnBootstrap on @RequestScoped classes. Options: --json (issue list for CI/IDE), --export <name>.
hono-forge client <entry>hover
Writes a typed client route map โ€” the generated file declares interface AppRoutes for createClient<AppRoutes>(baseUrl). Each controller is imported from its own source file โ€” @Controller records where the class is declared, so the output compiles even when controllers aren't re-exported from the entry module. Options: --out <file> (default ./routes.gen.ts), --name <ident> (interface name), --import-path <p> (opt out of per-file imports and treat one module as a barrel for every controller), --export <name>. Re-run as a codegen step whenever routes change.
hono-forge help <command>hover
Shows per-command flags and examples โ€” also available as hono-forge <command> --help. Unknown commands get a 'did you mean' suggestion.
how it works

Request flow.

Every request passes through a clean, layered middleware pipeline before reaching your controller.

01

Route Registration

HonoRouteBuilder.build() โ€” or buildModule() for @Module trees โ€” reads decorator metadata (@Controller, @Get, @Post, @Sse, @WebSocket, @ChannelRoute, etc.) and registers all routes onto a Hono app instance at startup.

02

Trace ID & onRequestStart

A trace/correlation ID is assigned from X-Request-ID or auto-generated UUID and echoed back on the response. The onRequestStart hook fires here โ€” use it to start OTel spans or attach logger context.

03

Middleware

@Middleware/@Use at class or method level, plus built-ins (@Cors, @Compress, @SecureHeaders, @PrettyJson, @JwtAuth), are prepended to the handler chain. Runs before guards and rate limiters.

04

Rate Limiting

@RateLimit checks the built-in in-memory limiter (or your pluggable rateLimiterFactory). Emits X-RateLimit-Limit/Remaining/Reset headers and returns 429 with Retry-After if exceeded.

05

Auth Guards

@RequireAuth, @RequireRole, @RequirePermission are passed to your pluggable guardExecutor. HttpException thrown by guards passes through unchanged; other errors map to 401 for "Unauthorized", 403 for "Forbidden". @Public skips all guards.

06

Dedupe & Idempotency

@Idempotent replays cached responses for duplicate Idempotency-Key requests. @SingleFlight coalesces concurrent identical requests so the handler runs once.

07

Parameter Resolution

Helper functions read and validate context values inside your handler: Body(c, schema) and Query(c, schema) validate via Zod (400 on failure), plus Param, User, Ip, Device, Headers, Cookie, UploadedFile and more. SSE handlers receive the stream as a second argument.

08

Controller Method

Your business logic executes with DI-injected services resolved from the container. Request-scoped instances (@RequestScoped) are created fresh and shared within this request. getContext() and getTraceId() are available anywhere in the call chain.

09

Response & Cleanup

Return values serialize to JSON; a returned Response passes through untouched. @Status/@NoContent/@Header/@Redirect shape the response. Errors flow through @Catch/@UseFilters method filters โ†’ class filters โ†’ onError โ†’ HttpException. onDestroy() runs on request-scoped instances, and requestLogger fires with method, path, statusCode, durationMs, ip, traceId.

try it

Generated flows, ready to run.

hono-forge can emit ready-to-execute .flow.yaml files straight from your route metadata โ€” CRUD chains, sample bodies from @Validate/@ApiBody schemas (multipart form-data for z.file() fields), query params from @ApiQuery, typed path-param variables, response assertions from @ApiResponse, auth placeholders (Bearer headers โ€” or ?token= on WS nodes, since sockets can't carry headers), and SSE/WS nodes. Import them into api-tester (our Electron API client) and hit Run.

todo.flow.yaml
CRUD chain โ€” list โ†’ create (extracts {{id}}) โ†’ get โ†’ delete, with auth + validation assertions.
Download todo.flow.yaml
feed.flow.yaml
Live SSE feed node โ€” listens to the server-sent event stream for 10s.
Download feed.flow.yaml

Using the flows: start the example app bun examples/todo-api/index.ts, open api-tester โ†’ Import flow โ†’ pick a file โ†’ set the {{token}} variable โ†’ Run. Generate your own with bunx hono-forge flows ./src/app.module.ts, or point the importer at /openapi.json for REST+SSE+WS node generation without any flow files.

changelog

What's new

All notable changes to hono-forge are documented here. Follows Keep a Changelog and Semantic Versioning.Loading...

โœจ

Modules, filters, channels, typed client

@Module + buildModule, @Catch/@UseFilters, @ChannelRoute, generateClientTypes + createClient

feedback

Share Your Thoughts

Have feedback, questions, or suggestions? Drop a message below.

messages

Recent Feedback

See what others have shared.