From a8145ff5bb7f36dcf880998288a857eb9d1d8acf Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Sat, 24 May 2025 21:04:32 -0500 Subject: [PATCH] docs: update README with detailed usage examples and API reference for injectServices and useService --- README.md | 517 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 517 insertions(+) diff --git a/README.md b/README.md index d41b5b8..a5b77ce 100644 --- a/README.md +++ b/README.md @@ -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(); + +// 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(c: Context, serviceType: ServiceIdentifier): 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 { + // 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 { + // Implementation here + return Promise.resolve([]); + } +} + +@injectable() +class Logger { + log(message: string): void { + console.log(message); + } +} + +const IConfigService = createServiceIdentifier(); +const IUserService = createServiceIdentifier(); +const ILogger = createServiceIdentifier(); + +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; +} + +interface IUserService { + getUser(id: string): Promise; +} + +@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 { + 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 { + this.logger.log(`Getting user with id: ${id}`); + return await this.userRepository.findById(id); + } +} + +// Service registration +const ILogger = createServiceIdentifier(); +const IUserRepository = createServiceIdentifier(); +const IUserService = createServiceIdentifier(); + +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; +} + +@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(); +const SmsNotification = createServiceIdentifier(); + +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; +} + +@injectable() +class Database implements IDatabase { + constructor(@inject(IDatabaseConfig) private config: IDatabaseConfig) {} + + async query(sql: string): Promise { + // Database query implementation + return []; + } +} + +const IDatabaseConfig = createServiceIdentifier(); +const IDatabase = createServiceIdentifier(); + +services.addSingleton(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; + createUser(data: CreateUserRequest): Promise; + updateUser(id: string, data: UpdateUserRequest): Promise; + deleteUser(id: string): Promise; +} + +// โŒ 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(); + +@injectable() +class UserService { + constructor(@inject(ILogger) private logger: ILogger) {} + + async getUser(id: string): Promise { + 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