hono-injector
v1.1.0
Published
Zero-overhead Dependency Injection and Class-based Routing for Hono.
Maintainers
Readme
Hono Injector
Inversify + Hono = ⚡️
Decorator-based routing and dependency injection for Hono, heavily inspired by inversify-express-utils.
Features
- 🏗 Class-based Controllers: Organize your routes using classes.
- 💉 Dependency Injection: Full support for InversifyJS.
- 🎨 Decorators:
@controller,@httpGet,@middleware, and more. - 🚀 Lightweight: Built on top of Hono's blazing fast router.
- ⚡️ Zero Runtime Reflection: Route handlers are compiled at startup for maximum performance.
Architecture & Design Deep Dive | Changelog
Installation
npm install hono-injector hono inversify reflect-metadataQuick Start
- Setup your container
import 'reflect-metadata';
import { Container } from 'inversify';
import { Hono } from 'hono';
import { registerControllers } from 'hono-injector';
// Import your controllers somewhere so decorators run!
import { UserController } from './controllers/UserController';
const container = new Container();
// bind your services...
// container.bind(UserService).toSelf();
const app = new Hono();
// 1. Basic Registration (Defaults)
registerControllers(app, container, [UserController]);
// 2. Advanced Registration (With Options)
import { RegisterOptions } from 'hono-injector';
const options: RegisterOptions = {
prefix: '/api/v1',
debug: true,
// globalMiddleware: [authMiddleware]
};
registerControllers(app, container, [UserController], options);
export default app;Configuration
The registerControllers function accepts an optional options object:
interface RegisterOptions {
/**
* Global prefix for all controllers.
* Example: '/api/v1' -> /api/v1/users
*/
prefix?: string;
/**
* Middleware to apply to ALL controllers registered here.
* Useful for global auth, logging, or CORS.
*/
globalMiddleware?: MiddlewareHandler[];
/**
* Enable debug logging to see mapped routes at startup.
* Default: false
*/
debug?: boolean;
}- Create a Controller
import { controller, httpGet, ctx } from 'hono-injector';
import { Context } from 'hono';
import { inject } from 'inversify';
@controller('/users')
export class UserController {
constructor(@inject(UserService) private userService: UserService) {}
@httpGet('/')
getUsers(c: Context) {
return c.json(this.userService.getAll());
}
@httpGet('/:id')
getUser(@ctx() c: Context) {
const id = c.req.param('id');
return c.json(this.userService.getById(id));
}
}- Using Middleware
You can apply middleware at the controller or method level.
import { controller, httpPost, middleware } from 'hono-injector';
import { createMiddleware } from 'hono/factory';
// Example middleware
const logger = createMiddleware(async (c, next) => {
console.log(`[${c.req.method}] ${c.req.url}`);
await next();
});
const auth = createMiddleware(async (c, next) => {
// auth logic...
await next();
});
// Apply to all routes in this controller
@controller('/orders', [logger])
export class OrderController {
constructor(@inject(OrderService) private orderService: OrderService) {}
// Apply specific middleware to this route
@httpPost('/')
@middleware(auth)
createOrder(c: Context) {
return c.json({ message: 'Order created' });
}
}Advanced Usage
Dynamic Routing
If you need to define a route dynamically (e.g., method comes from config), you can use the generic @route decorator.
import { route } from 'hono-injector';
class WebhookController {
@route('post', '/webhook')
handleWebhook(c: Context) {
// ...
}
}Dependency Injection with Request Scope
All requests automatically get a Child Container. This means you can bind services in inRequestScope() to share state (like a user context or transaction) across services for a single request.
// 1. Bind a Service in Request Scope
container.bind(UserContext).toSelf().inRequestScope();
// 2. Inject it anywhere
@injectable()
class OrderService {
constructor(@inject(UserContext) private userCtx: UserContext) {}
create() {
// This userCtx is unique to the current request!
console.log(this.userCtx.currentUser);
}
}API
Class Decorators
@controller(path, middleware?)
Method Decorators
@httpGet(path, middleware?)@httpPost(path, middleware?)@httpPut(path, middleware?)@httpDelete(path, middleware?)@httpPatch(path, middleware?)
Parameter Decorators
@ctx()- Inject Hono Context@body(schema?)- Inject parsed body (optional validation schema)@param(name)- Inject path parameter@query(name)- Inject query parameter@header(name)- Inject header value
Recipes & Integrations
Validation with Zod
You can pass a validation schema directly to the @body() decorator. The library gracefully handles objects with a .parse() or .validate() method (Duck Typing).
import { z } from 'zod'; // npm i zod
const CreateUserSchema = z.object({
username: z.string().min(3),
email: z.string().email(),
});
@controller('/users')
class UserController {
@httpPost('/')
create(@body(CreateUserSchema) body: z.infer<typeof CreateUserSchema>) {
// Body is guaranteed to be valid here
return c.json({ created: true });
}
}Global Error Handling (Zod Example)
Hook into the onError callback to handle validation errors globally.
import { z } from 'zod';
registerControllers(app, container, [UserController], {
onError: (err, c) => {
if (err instanceof z.ZodError) {
return c.json({
success: false,
error: 'Validation Error',
details: err.flatten()
}, 400);
}
// Re-throw or handle other errors
throw err;
}
});License
MIT
