1.0.0 โ€ข Published 2 months ago

@stevanfreeborn/hono-netdi v1.0.0

Weekly downloads
-
License
MIT
Repository
github
Last release
2 months ago

hono-netdi

pull_request codecov publish semantic-release: angular NPM License NPM Version NPM Downloads

A powerful dependency injection middleware for Hono.js, inspired by .NET's dependency injection system. This library seamlessly integrates @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

npm install @stevanfreeborn/hono-netdi hono
yarn add @stevanfreeborn/hono-netdi hono
pnpm add @stevanfreeborn/hono-netdi hono

Quick Start

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:

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:

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:

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:

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>;
}

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,
    @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:

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:

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:

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:

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:

// โœ… 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:

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:

// 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:

// โœ… 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:

// โœ… 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:

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:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

License

This project is licensed under the MIT License - see the LICENSE.md file for details.

Related Projects

  • @stevanfreeborn/netdi - The core dependency injection container
  • Hono.js - The fast, lightweight web framework this middleware is designed for

Support

If you encounter any issues or have questions:

  1. Check the troubleshooting section above
  2. Search existing GitHub issues
  3. Create a new issue with a minimal reproduction case