2025-05-24 16:40:55 -05:00
# hono-netdi
2025-05-24 21:04:32 -05:00
A powerful dependency injection middleware for [Hono.js ](https://honojs.dev/ ), inspired by .NET's dependency injection system. This library seamlessly integrates [@stevanfreeborn/netdi ](https://github.com/StevanFreeborn/netdi ) with Hono applications, providing automatic service scope management and clean resource disposal for each HTTP request.
## Features
- 🔄 **Automatic Service Scope Management** - Creates and disposes service scopes per request
- 🧹 **Resource Cleanup** - Ensures proper disposal of scoped services to prevent memory leaks
- 🏗️ **Type-Safe DI** - Full TypeScript support with strongly-typed service resolution
- 🚀 **Hono Integration** - Seamless integration with Hono's middleware system
- 📦 **Lightweight** - Minimal overhead with clean, simple API
- 🔧 **Flexible** - Supports all service lifetimes (singleton, scoped, transient)
## Installation
```bash
npm install @stevanfreeborn/hono-netdi hono
```
```bash
yarn add @stevanfreeborn/hono-netdi hono
```
```bash
pnpm add @stevanfreeborn/hono-netdi hono
```
## Quick Start
```typescript
import { Hono } from 'hono' ;
2025-05-24 21:05:12 -05:00
import {
ServiceCollection ,
createServiceIdentifier ,
injectable ,
injectServices ,
useService ,
} from '@stevanfreeborn/hono-netdi' ;
2025-05-24 21:04:32 -05:00
// Define your service interface and implementation
interface IUserService {
getUser ( id : string ) : Promise < { id : string ; name : string } > ;
}
@injectable ()
class UserService implements IUserService {
async getUser ( id : string ) {
return { id , name : `User ${ id } ` };
}
}
// Create service identifier
const IUserService = createServiceIdentifier < IUserService >();
// Configure dependency injection
const services = new ServiceCollection ();
services . addScoped ( IUserService , UserService );
const serviceProvider = services . build ();
// Create Hono app with DI middleware
const app = new Hono ();
app . use ( injectServices ( serviceProvider ));
// Use services in your routes
2025-05-24 21:05:12 -05:00
app . get ( '/users/:id' , async c => {
2025-05-24 21:04:32 -05:00
const userService = useService ( c , IUserService );
const user = await userService . getUser ( c . req . param ( 'id' ));
return c . json ( user );
});
export default app ;
```
## API Reference
### `injectServices(serviceProvider: IServiceProvider): MiddlewareHandler`
Creates a Hono middleware that manages dependency injection service scopes for each request.
**Parameters:**
- `serviceProvider` - The root service provider from which to create scoped instances
**Returns:**
- A Hono middleware handler that manages service scope lifecycle
**Behavior:**
- Creates a new service scope at the beginning of each request
- Stores the scope in the Hono context for access by route handlers
- Automatically disposes the scope when the request completes
- Ensures proper cleanup even if errors occur during request processing
### `useService<T>(c: Context, serviceType: ServiceIdentifier<T>): T`
Retrieves a service instance from the current request's dependency injection scope.
**Parameters:**
- `c` - The Hono context containing the service scope
- `serviceType` - The service identifier used to resolve the service instance
**Returns:**
- The resolved service instance of type `T`
**Throws:**
- `Error` - When service scope is not found in context (middleware not configured)
- `Error` - When service scope is invalid
- `Error` - When the requested service cannot be resolved
## Required Decorators
This library works with netdi's decorator system. You must use the appropriate decorators:
### `@injectable()`
Mark all service classes with the `@injectable()` decorator:
```typescript
import { injectable } from '@stevanfreeborn/hono-netdi' ;
@injectable ()
class UserService implements IUserService {
async getUser ( id : string ) : Promise < User > {
// Implementation
}
}
```
### `@inject()`
Use `@inject()` for constructor parameters that should be injected:
```typescript
import { injectable , inject } from '@stevanfreeborn/hono-netdi' ;
@injectable ()
class UserService implements IUserService {
constructor (
@inject ( IUserRepository ) private userRepository : IUserRepository ,
2025-05-24 21:05:12 -05:00
@inject ( ILogger ) private logger : ILogger ,
2025-05-24 21:04:32 -05:00
) {}
}
```
**Note:** Without these decorators, netdi cannot properly resolve dependencies and will throw runtime errors.
**Note:** The argument passed to the `@inject()` decorator should be the service identifier created with `createServiceIdentifier()` that was used to register the service.
## Advanced Usage
### Service Lifetimes
The library supports all service lifetimes provided by netdi:
```typescript
import { ServiceCollection , createServiceIdentifier , injectable } from '@stevanfreeborn/hono-netdi' ;
@injectable ()
class ConfigService {
getConnectionString () : string {
return process . env . DB_CONNECTION_STRING || '' ;
}
}
@injectable ()
class UserService {
getUsers () : Promise < User [] > {
// Implementation here
return Promise . resolve ([]);
}
}
@injectable ()
class Logger {
log ( message : string ) : void {
console . log ( message );
}
}
const IConfigService = createServiceIdentifier < ConfigService >();
const IUserService = createServiceIdentifier < UserService >();
const ILogger = createServiceIdentifier < Logger >();
const services = new ServiceCollection ();
// Singleton - One instance for the entire application
services . addSingleton ( IConfigService , ConfigService );
// Scoped - One instance per request scope
services . addScoped ( IUserService , UserService );
// Transient - New instance every time it's requested
services . addTransient ( ILogger , Logger );
```
### Service Dependencies
Services can depend on other services through constructor injection using the `@inject()` decorator:
```typescript
import { injectable , inject , createServiceIdentifier } from '@stevanfreeborn/hono-netdi' ;
interface ILogger {
log ( message : string ) : void ;
}
interface IUserRepository {
findById ( id : string ) : Promise < User >;
}
interface IUserService {
getUser ( id : string ) : Promise < User >;
}
2025-05-24 22:00:56 -05:00
const ILogger = createServiceIdentifier < ILogger >();
const IUserRepository = createServiceIdentifier < IUserRepository >();
const IUserService = createServiceIdentifier < IUserService >();
2025-05-24 21:04:32 -05:00
@injectable ()
class Logger implements ILogger {
log ( message : string ) {
console . log ( `[ ${ new Date (). toISOString () } ] ${ message } ` );
}
}
@injectable ()
class UserRepository implements IUserRepository {
constructor ( @inject ( ILogger ) private logger : ILogger ) {}
async findById ( id : string ) : Promise < User > {
this . logger . log ( `Finding user with id: ${ id } ` );
// Database logic here
return { id , name : `User ${ id } ` };
}
}
@injectable ()
class UserService implements IUserService {
constructor (
@inject ( IUserRepository ) private userRepository : IUserRepository ,
2025-05-24 21:05:12 -05:00
@inject ( ILogger ) private logger : ILogger ,
2025-05-24 21:04:32 -05:00
) {}
async getUser ( id : string ) : Promise < User > {
this . logger . log ( `Getting user with id: ${ id } ` );
return await this . userRepository . findById ( id );
}
}
const services = new ServiceCollection ();
services . addSingleton ( ILogger , Logger );
services . addScoped ( IUserRepository , UserRepository );
services . addScoped ( IUserService , UserService );
```
### Multiple Service Implementations
Register multiple implementations of the same interface:
```typescript
import { injectable , createServiceIdentifier } from '@stevanfreeborn/hono-netdi' ;
interface INotificationService {
send ( message : string ) : Promise < void >;
}
@injectable ()
class EmailNotificationService implements INotificationService {
async send ( message : string ) {
console . log ( `Email: ${ message } ` );
}
}
@injectable ()
class SmsNotificationService implements INotificationService {
async send ( message : string ) {
console . log ( `SMS: ${ message } ` );
}
}
const EmailNotification = createServiceIdentifier < INotificationService >();
const SmsNotification = createServiceIdentifier < INotificationService >();
services . addScoped ( EmailNotification , EmailNotificationService );
services . addScoped ( SmsNotification , SmsNotificationService );
// Use in routes
2025-05-24 21:05:12 -05:00
app . post ( '/notify' , async c => {
2025-05-24 21:04:32 -05:00
const emailService = useService ( c , EmailNotification );
const smsService = useService ( c , SmsNotification );
2025-05-24 21:05:12 -05:00
2025-05-24 21:04:32 -05:00
await emailService . send ( 'Hello via email!' );
await smsService . send ( 'Hello via SMS!' );
2025-05-24 21:05:12 -05:00
2025-05-24 21:04:32 -05:00
return c . json ({ success : true });
});
```
### Factory Registration
Register services using factory functions for complex initialization:
```typescript
import { injectable , inject , createServiceIdentifier } from '@stevanfreeborn/hono-netdi' ;
interface IDatabaseConfig {
connectionString : string ;
timeout : number ;
}
interface IDatabase {
query ( sql : string ) : Promise < any [] >;
}
@injectable ()
class Database implements IDatabase {
constructor ( @inject ( IDatabaseConfig ) private config : IDatabaseConfig ) {}
async query ( sql : string ) : Promise < any [] > {
// Database query implementation
return [];
}
}
const IDatabaseConfig = createServiceIdentifier < IDatabaseConfig >();
const IDatabase = createServiceIdentifier < IDatabase >();
services . addSingleton < IDatabaseConfig >( IDatabaseConfig , () => ({
connectionString : process.env.DB_CONNECTION_STRING ! ,
2025-05-24 21:05:12 -05:00
timeout : 30000 ,
2025-05-24 21:04:32 -05:00
}));
2025-05-24 21:05:12 -05:00
services . addScoped ( IDatabase , provider => {
2025-05-24 21:04:32 -05:00
const config = provider . getService ( IDatabaseConfig );
return new Database ( config );
});
```
### Error Handling
The middleware automatically handles service scope disposal even when errors occur:
```typescript
2025-05-24 21:05:12 -05:00
app . get ( '/error-example' , async c => {
2025-05-24 21:04:32 -05:00
const userService = useService ( c , IUserService );
2025-05-24 21:05:12 -05:00
2025-05-24 21:04:32 -05:00
try {
// This might throw an error
const user = await userService . getUser ( 'invalid-id' );
return c . json ( user );
} catch ( error ) {
// Service scope will still be properly disposed
return c . json ({ error : 'User not found' }, 404 );
}
});
```
### Custom Middleware Order
The `injectServices` middleware should be registered early in your middleware chain:
```typescript
const app = new Hono ();
// Register DI middleware first
app . use ( injectServices ( serviceProvider ));
// Then other middleware
app . use ( cors ());
app . use ( logger ());
// Routes can now use services
2025-05-24 21:05:12 -05:00
app . get ( '/' , c => {
2025-05-24 21:04:32 -05:00
const service = useService ( c , IMyService );
return c . json ( service . getData ());
});
```
## Best Practices
### Service Interface Design
Define clear interfaces for your services:
```typescript
// ✅ Good - Clear interface with specific methods
interface IUserService {
getUser ( id : string ) : Promise < User >;
createUser ( data : CreateUserRequest ) : Promise < User >;
updateUser ( id : string , data : UpdateUserRequest ) : Promise < User >;
deleteUser ( id : string ) : Promise < void >;
}
// ❌ Avoid - Vague or overly broad interfaces
interface IService {
doSomething ( data : any ) : any ;
}
```
### Use Decorators Properly
Always use `@injectable()` on service classes and `@inject()` for dependencies:
```typescript
import { injectable , inject , createServiceIdentifier } from '@stevanfreeborn/netdi' ;
const ILogger = createServiceIdentifier < ILogger >();
@injectable ()
class UserService {
constructor ( @inject ( ILogger ) private logger : ILogger ) {}
2025-05-24 21:05:12 -05:00
2025-05-24 21:04:32 -05:00
async getUser ( id : string ) : Promise < User > {
this . logger . log ( `Getting user ${ id } ` );
// Implementation
}
}
```
### Service Lifetime Selection
Choose appropriate service lifetimes:
```typescript
// Singleton - For stateless services, configuration, caches
services . addSingleton ( IConfigService , ConfigService );
services . addSingleton ( ILogger , Logger );
// Scoped - For services that maintain state per request
services . addScoped ( IUserService , UserService );
services . addScoped ( IDatabaseContext , DatabaseContext );
// Transient - For lightweight, stateless services
services . addTransient ( IValidator , Validator );
services . addTransient ( IMapper , Mapper );
```
### Dependency Management
Keep dependencies minimal and well-defined:
```typescript
// ✅ Good - Clear, minimal dependencies
@injectable ()
class UserService implements IUserService {
constructor (
@inject ( IUserRepository ) private userRepository : IUserRepository ,
2025-05-24 21:05:12 -05:00
@inject ( ILogger ) private logger : ILogger ,
2025-05-24 21:04:32 -05:00
) {}
}
// ❌ Avoid - Too many dependencies (consider refactoring)
@injectable ()
class UserService implements IUserService {
constructor (
@inject ( IRepo1 ) private repo1 : IRepo1 ,
@inject ( IRepo2 ) private repo2 : IRepo2 ,
@inject ( IService1 ) private service1 : IService1 ,
@inject ( IService2 ) private service2 : IService2 ,
@inject ( IService3 ) private service3 : IService3 ,
// ... too many dependencies
) {}
}
```
## Troubleshooting
### Service scope not found error
**Solution:** Ensure `injectServices` middleware is registered before routes that use `useService` :
```typescript
// ✅ Correct order
app . use ( injectServices ( serviceProvider ));
2025-05-24 21:05:12 -05:00
app . get ( '/' , c => useService ( c , IMyService ));
2025-05-24 21:04:32 -05:00
// ❌ Wrong order
2025-05-24 21:05:12 -05:00
app . get ( '/' , c => useService ( c , IMyService ));
2025-05-24 21:04:32 -05:00
app . use ( injectServices ( serviceProvider ));
```
### Service not registered error
**Solution:** Ensure the service is registered in your service collection:
```typescript
const services = new ServiceCollection ();
services . addScoped ( IMyService , MyService ); // Register the service
const serviceProvider = services . build ();
```
### Decorator errors
**Solution:** Ensure your `tsconfig.json` has decorators enabled:
```json
{
"compilerOptions" : {
"experimentalDecorators" : true ,
"emitDecoratorMetadata" : true
}
}
```
## License
This project is licensed under the MIT License - see the [LICENSE.md ](LICENSE.md ) file for details.
## Related Projects
- [@stevanfreeborn/netdi ](https://github.com/StevanFreeborn/netdi ) - The core dependency injection container
- [Hono.js ](https://honojs.dev/ ) - The fast, lightweight web framework this middleware is designed for
## Support
If you encounter any issues or have questions:
1. Check the [troubleshooting section ](#troubleshooting ) above
2. Search existing [GitHub issues ](https://github.com/StevanFreeborn/hono-netdi/issues )
3. Create a new issue with a minimal reproduction case