Controllers, DI, modules, guards, SSE, WebSocket, channels, caching, events, scheduling, resilience, OpenAPI, typed client, and testing โ all declarative. Install one package. No boilerplate.
One package. No boilerplate. Focus on business logic.
Install one package, decorate your controllers, mount the routes.
$ bun add hono-forge hono zod
{
"compilerOptions": {
"target": "ESNext"
}
}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;
Every decorator and API available in hono-forge, organized by category. Hover any item to see a usage example.
Every request passes through a clean, layered middleware pipeline before reaching your controller.
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.
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.
@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.
@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.
@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.
@Idempotent replays cached responses for duplicate Idempotency-Key requests. @SingleFlight coalesces concurrent identical requests so the handler runs once.
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.
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.
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.
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.
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.
All notable changes to hono-forge are documented here. Follows Keep a Changelog and Semantic Versioning.Loading...
Have feedback, questions, or suggestions? Drop a message below.
See what others have shared.