Files
hono-netdi/README.md
T

533 lines
14 KiB
Markdown
Raw Normal View History

2025-05-24 16:40:55 -05:00
# hono-netdi
2025-05-24 22:17:18 -05:00
[![pull_request](https://github.com/StevanFreeborn/hono-netdi/actions/workflows/pull_request.yml/badge.svg)](https://github.com/StevanFreeborn/hono-netdi/actions/workflows/pull_request.yml)
[![codecov](https://codecov.io/gh/StevanFreeborn/hono-netdi/graph/badge.svg?token=RSPq92k1Mz)](https://codecov.io/gh/StevanFreeborn/hono-netdi)
[![publish](https://github.com/StevanFreeborn/hono-netdi/actions/workflows/publish.yml/badge.svg)](https://github.com/StevanFreeborn/hono-netdi/actions/workflows/publish.yml)
[![semantic-release: angular](https://img.shields.io/badge/semantic--release-angular-e10079?logo=semantic-release)](https://github.com/semantic-release/semantic-release)
![NPM License](https://img.shields.io/npm/l/%40stevanfreeborn%2Fhono-netdi)
![NPM Version](https://img.shields.io/npm/v/%40stevanfreeborn%2Fhono-netdi)
![NPM Downloads](https://img.shields.io/npm/dt/%40stevanfreeborn%2Fhono-netdi)
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';
// 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 => {
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,
) {}
}
```
**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>();
@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,
) {}
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 => {
const emailService = useService(c, EmailNotification);
const smsService = useService(c, SmsNotification);
2025-05-24 21:05:12 -05:00
await emailService.send('Hello via email!');
await smsService.send('Hello via SMS!');
2025-05-24 21:05:12 -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:05:12 -05:00
services.addScoped(IDatabase, provider => {
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 => {
const userService = useService(c, IUserService);
2025-05-24 21:05:12 -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 => {
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
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,
) {}
}
// ❌ 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));
// ❌ Wrong order
2025-05-24 21:05:12 -05:00
app.get('/', c => useService(c, IMyService));
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