v0.2.1 ยท npm package ยท open source

Hono with
NestJS-style decorators.

Controllers, DI, guards, SSE, WebSocket, channels, rate limiting, OpenAPI, structured errors, and trace IDs โ€” all declarative. Install one package. No boilerplate.

View on GitHub โ†—Quick start โ†’
$bun add hono-forge hono zod
Hono 4.0+Bun 1.0+TypeScript 5.0+ZodSSEWebSocketRedisOpenAPI 3.1
14k+
req / sec
50+
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
Type-safe parameter binding for body, query, and route params. Schema-first with full TypeScript inference.
@Body(schema)@Query(schema)@Param
๐Ÿ“ก
SSE & WebSocket
First-class streaming with @Sse and @WebSocket decorators. Pluggable upgrader for any Hono runtime.
@Sse@WebSocket@SseStream
๐Ÿ”
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 custom window, max requests, and key generators. Pluggable for any backend.
@RateLimitcustom keyGen
๐Ÿ“Š
Request Logging
Pluggable logger with IP extraction, device detection, and duration tracking. Inject IP/device directly into handlers.
@Ip@Device@UserAgent
๐Ÿ’‰
Dependency Injection
Lightweight DI container with singleton, transient, and request-scoped lifetimes. Lifecycle hooks, circular dependency detection, and auto-resolution.
@Injectable@Singleton@RequestScoped@Inject
๐Ÿ“–
OpenAPI 3.1 + Scalar
Auto-generate a full OpenAPI spec from your decorators. Serve interactive Scalar docs with one line.
OpenAPIGenerator@ApiDoc@ApiTags
โšก
Interceptors
Cross-cutting concerns without middleware clutter. Retry, timeout, transform, cache, and metrics decorators.
@Retry@Timeout@Transform@Cache@TrackMetrics
๐ŸŒ
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. Enable decorators
tsconfig.json
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}
3. Write your controller
controller.ts
import 'reflect-metadata';
import { Hono } 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')
class UserController {
  constructor(private userService: UserService) {}

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

  @Post() @RequireAuth()
  create(@Body(CreateUserSchema) body: any) {
    return this.userService.create(body);
  }

  @Patch('/:id') @RequireAuth()
  update(@Param('id') id: string, @Body(UpdateUserSchema) body: any) {
    return this.userService.update(id, body);
  }

  @Delete('/:id') @RequireAuth()
  remove(@Param('id') id: string) {
    return this.userService.delete(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.

routing
@Controller(path?, opts?)
Registers a class as a controller. Accepts basePath, platform, and version.
@Get(path?)
Maps method to HTTP GET.
@Post(path?)
Maps method to HTTP POST.
@Put(path?)
Maps method to HTTP PUT.
@Delete(path?)
Maps method to HTTP DELETE.
@Patch(path?)
Maps method to HTTP PATCH.
@Head(path?)
Registered as GET; Hono handles HEAD requests via fallback.
@Options(path?)
Maps method to HTTP OPTIONS.
@All(path?)
Matches all HTTP methods on the given path.
real-time
@Sse(path?)
Registers a GET endpoint for Server-Sent Events. Inject the stream via @SseStream().
@WebSocket(path?)
Registers a WebSocket upgrade endpoint. Handler returns { onOpen, onMessage, onClose }.
@Middleware(fn)
Attaches Hono middleware at class level (all routes) or method level (one route).
@Use(fn)
Alias for @Middleware โ€” familiar for NestJS/Express users.
built-in middleware
@Cors(opts?)
Applies CORS headers. Options match hono/cors โ€” origin, allowMethods, maxAge, etc.
@Compress(opts?)
Applies gzip/deflate response compression. Works at class or method level.
@SecureHeaders(opts?)
Adds security headers: CSP, HSTS, X-Frame-Options, X-Content-Type-Options, etc.
@PrettyJson(opts?)
Pretty-prints JSON responses when ?pretty query param is present.
auth & security
@RequireAuth()
Attaches AuthGuard โ€” your guardExecutor validates the token.
@RequireRole(...roles)
Requires user to have ONE of the specified roles.
@RequireAllRoles(...roles)
Requires user to have ALL specified roles.
@RequirePermission(...perms)
Requires ALL listed permissions.
@RequireAnyPermission(...perms)
Requires at least ONE of the listed permissions.
@Public()
Marks route as public, bypassing all auth guards.
@Private()
Marks route as internal-only. Excluded when build() is called with { excludePrivate: true }.
@RateLimit(opts)
Rate-limits the route. Options: max, windowMs, message, keyGenerator.
parameters
@Body(schema?)
Injects request body. Pass a Zod schema for validation โ€” returns 400 on failure.
@Query(schema?)
Injects query params. Pass a Zod object schema to validate and type the result.
@Param(name)
Injects a route path parameter by name.
@Headers(name)
Injects a request header by name.
@User()
Injects the authenticated user object set by the guard.
@Ip()
Injects the real client IP (CF-Connecting-IP โ†’ X-Real-IP โ†’ X-Forwarded-For).
@Device()
Injects detected device type: 'mobile' | 'tablet' | 'desktop' | 'bot'.
@UserAgent()
Injects the raw User-Agent header string.
@SseStream()
Injects the SSEStreamingApi stream inside an @Sse handler.
@Req()
Injects the Hono HonoRequest object.
@Res()
Injects the Hono Context object.
@ValidatedBody(schema)
Type-safe alias for @Body โ€” carries the Zod inferred type so no explicit annotation is needed.
@ValidatedQuery(schema)
Type-safe alias for @Query with full Zod inference.
@ValidatedParam(name, schema)
Type-safe alias for @Param with Zod validation and inferred type.
@UploadedFile(field)
Injects a single File from multipart form data by field name. Returns File | null.
@UploadedFiles(field?)
Injects all File objects for a field. Omit field to collect every file in the form.
@FormBody()
Injects the raw FormData object. Parsed once per request even with multiple file params.
channels (pub/sub)
channels.subscribe(ch, client)
Add a client to a channel. Client can be SseChannelClient or WsChannelClient.
channels.unsubscribe(ch, id)
Remove a client from a channel by clientId.
channels.publish(ch, event, data)
Broadcast an event+data payload to all subscribers of a channel.
channels.use(adapter)
Swap the channel adapter at startup. Default: InMemoryChannelAdapter.
new RedisChannelAdapter(pub, sub)
Redis-backed adapter for multi-instance SSE/WS broadcasting.
interceptors
@Retry(opts)
Retries the method on failure. Options: attempts, delay, backoff (linear | exponential).
@Timeout(ms)
Rejects with an error if the method takes longer than ms milliseconds.
@Transform(fn)
Applies a transform function to the return value before sending.
@Cache(opts)
Stores cache metadata. Options: ttl, key. Integrate with your own cache layer.
@TrackMetrics(opts)
Records method duration via this.metrics.trackMethodDuration(). Options: name.
@Throttle(ms)
Prevents the method from being called more than once per ms window. Throws if called too soon.
@Memoize(opts?)
Caches the return value in memory. Options: ttl (ms), scope ("global" | "request"). Use scope: "request" on singletons returning user-specific data.
@ValidateResult(schema)
Validates the return value against a Zod schema. Throws ZodError on mismatch.
@Audit(opts)
Logs an audit entry via this.logger before the method runs. Options: action.
@Transaction(exec?)
Wraps method in this.db.transaction(). Pass a custom TransactionExecutor for Prisma ($transaction) or Kysely.
api documentation
@ApiTags(...tags)
Groups controller endpoints under named tags in the OpenAPI spec.
@ApiDoc(opts)
Documents an endpoint. Options: summary, description.
@ApiResponse(status, msg)
Describes a possible HTTP response for this endpoint.
@ApiDeprecated()
Marks the endpoint as deprecated in the generated spec.
OpenAPIGenerator.generate()
Generates a full OpenAPI 3.1 spec from your controller classes.
OpenAPIGenerator.mount()
Serves /openapi.json and Scalar UI at /docs on a Hono app.
dependency injection
@Injectable()
Registers a class in the DI container. Required for constructor injection.
@Singleton()
Returns the same instance on every resolve().
@RequestScoped()
Fresh instance per request, destroyed automatically when the request ends. Shared within the same request.
@Stateless()
No-op marker for @Singleton classes that hold no mutable per-request state. Documents intent; enforceable by future linting tools.
@Inject(token)
Injects by string or symbol token โ€” useful for interfaces or config values.
container.boot()
Calls onInit() on all registered singletons that implement OnInit. Call once at app startup before starting the server.
container.shutdown()
Calls onDestroy() on all singletons in reverse registration order. Call in SIGTERM/SIGINT handler.
container.runInScope(fn)
Runs fn in a new request scope. onDestroy is called on all scoped instances in the finally block. Called automatically by HonoRouteBuilder.
container.registerInstance(token, value)
Registers a pre-built external object (Drizzle, Prisma, Redis, etc.) under a symbol token.
error handling
HttpException
Structured HTTP error with status, code, message, and optional meta payload. Auto-handled by the route builder.
HttpException.badRequest(msg?)
400 BAD_REQUEST
HttpException.unauthorized(msg?)
401 UNAUTHORIZED
HttpException.forbidden(msg?)
403 FORBIDDEN
HttpException.notFound(msg?)
404 NOT_FOUND
HttpException.conflict(msg?)
409 CONFLICT
HttpException.unprocessable(msg?)
422 UNPROCESSABLE_ENTITY
HttpException.tooManyRequests(msg?)
429 TOO_MANY_REQUESTS
HttpException.internal(msg?)
500 INTERNAL_SERVER_ERROR
HttpException.serviceUnavailable(msg?)
503 SERVICE_UNAVAILABLE
observability & config
getTraceId()
Returns the active trace/correlation ID. Callable from anywhere in the call chain โ€” services, repos, loggers โ€” without passing it explicitly.
runWithTraceId(id, fn)
Runs fn within a trace context. Use when running handlers outside HonoRouteBuilder.
onRequestStart hook
Called before middleware and guards on every request. Receives { method, path, traceId, ip, userAgent }. Use for OTel spans or logger context.
strictValidation option
Enforces @Body() schema usage on mutation routes. Values: "warn" (default) | "error" (throws at build time) | "off".
exposeStack option
Controls stack trace exposure in HttpException responses. Values: false (default, safe) | true | "development" (only when NODE_ENV != production).
how it works

Request flow.

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

01

Route Registration

HonoRouteBuilder.build() reads decorator metadata (@Controller, @Get, @Post, @Sse, @WebSocket, 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 decorators at class or method level are resolved and prepended to the handler chain. Runs before guards and rate limiters.

04

Rate Limiting

@RateLimit triggers the pluggable rateLimiterFactory. Returns 429 if the limit is exceeded before the request reaches your handler.

05

Auth Guards

@RequireAuth, @RequireRole, @RequirePermission are passed to your pluggable guardExecutor. Throws 401 for "Unauthorized", 403 for "Forbidden". @Public skips all guards.

06

Parameter Resolution

@Body(schema) and @Query(schema) validate via Zod โ€” returns 400 on failure. @Param, @User, @Ip, @Device, @UserAgent, @Headers, @Req, @Res inject context values. @SseStream injects the SSE stream for streaming handlers.

07

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. getTraceId() is available anywhere in the call chain.

08

Response & Cleanup

Return value is serialized to JSON. HttpException is auto-caught and returned as structured JSON at the correct status. onDestroy() is called on all @RequestScoped instances. requestLogger fires with method, path, statusCode, durationMs, ip, traceId.

changelog

What's new

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

๐Ÿ”ง

@Stateless runtime enforcement

Resolved @Stateless @Singleton instances are now wrapped in a Proxy

feedback

Share Your Thoughts

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

messages

Recent Feedback

See what others have shared.