docs: update README with detailed usage examples and API reference for injectServices and useService
This commit is contained in:
@@ -1,2 +1,519 @@
|
||||
# hono-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';
|
||||
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
|
||||
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,
|
||||
@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>;
|
||||
}
|
||||
|
||||
@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,
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
// Service registration
|
||||
const ILogger = createServiceIdentifier<ILogger>();
|
||||
const IUserRepository = createServiceIdentifier<IUserRepository>();
|
||||
const IUserService = createServiceIdentifier<IUserService>();
|
||||
|
||||
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
|
||||
app.post('/notify', async (c) => {
|
||||
const emailService = useService(c, EmailNotification);
|
||||
const smsService = useService(c, SmsNotification);
|
||||
|
||||
await emailService.send('Hello via email!');
|
||||
await smsService.send('Hello via SMS!');
|
||||
|
||||
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!,
|
||||
timeout: 30000
|
||||
}));
|
||||
|
||||
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
|
||||
app.get('/error-example', async (c) => {
|
||||
const userService = useService(c, IUserService);
|
||||
|
||||
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
|
||||
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) {}
|
||||
|
||||
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,
|
||||
@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));
|
||||
app.get('/', (c) => useService(c, IMyService));
|
||||
|
||||
// ❌ Wrong order
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user