Dependency Injection
Dependency Injection Guide
Section titled “Dependency Injection Guide”Learn how to use Veloce’s built-in dependency injection system effectively.
Table of Contents
Section titled “Table of Contents”- Introduction
- Basic Usage
- Dependency Scopes
- Registering Dependencies
- Advanced Patterns
- Testing with DI
- Best Practices
Introduction
Section titled “Introduction”Dependency Injection (DI) is a design pattern that helps you write modular, testable, and maintainable code. Veloce includes a built-in DI container that manages the lifecycle of your dependencies.
Benefits
Section titled “Benefits”- Testability: Easy to mock dependencies in tests
- Modularity: Clear separation of concerns
- Maintainability: Changes to dependencies don’t affect consumers
- Flexibility: Easy to swap implementations
Basic Usage
Section titled “Basic Usage”Defining a Service
Section titled “Defining a Service”class DatabaseService { async query(sql: string) { // Database logic return []; }}
class UserService { async getUsers() { return [ { id: 1, name: 'John' }, { id: 2, name: 'Jane' }, ]; }
async getUserById(id: string) { const users = await this.getUsers(); return users.find(u => u.id === parseInt(id)); }}Injecting Dependencies
Section titled “Injecting Dependencies”Use the @Depends() decorator to inject dependencies into your handlers:
@Controller('/users')class UserController { @Get('/') async listUsers(@Depends(UserService) userService: UserService) { return await userService.getUsers(); }
@Get('/:id') async getUser( @Param('id') id: string, @Depends(UserService) userService: UserService ) { const user = await userService.getUserById(id); if (!user) { throw new HTTPException(404, 'User not found'); } return user; }}Registering Services
Section titled “Registering Services”Register your services with the DI container:
const app = new Veloce();const container = app.getContainer();
// Register servicescontainer.register(DatabaseService, { scope: 'singleton' });container.register(UserService, { scope: 'request' });
// Include controllerapp.include(UserController);Dependency Scopes
Section titled “Dependency Scopes”Veloce supports three dependency scopes:
Singleton
Section titled “Singleton”One instance for the entire application lifecycle.
container.register(DatabaseService, { scope: 'singleton' });Use for:
- Database connections
- Configuration services
- Stateless services
- Caches
Example:
class ConfigService { private config: Record<string, any>;
constructor() { this.config = { apiKey: process.env.API_KEY, dbUrl: process.env.DATABASE_URL, }; }
get(key: string) { return this.config[key]; }}
// Register as singletoncontainer.register(ConfigService, { scope: 'singleton' });Request
Section titled “Request”One instance per HTTP request (default).
container.register(UserService, { scope: 'request' });Use for:
- Request-specific services
- Services that need request context
- Services with request-scoped state
Example:
class RequestLogger { private logs: string[] = [];
log(message: string) { this.logs.push(`[${new Date().toISOString()}] ${message}`); }
getLogs() { return this.logs; }}
// Register as request-scopedcontainer.register(RequestLogger, { scope: 'request' });
@Controller('/api')class ApiController { @Get('/data') async getData(@Depends(RequestLogger) logger: RequestLogger) { logger.log('Fetching data'); // ... fetch data logger.log('Data fetched'); return { logs: logger.getLogs() }; }}Transient
Section titled “Transient”New instance every time it’s injected.
container.register(TemporaryService, { scope: 'transient' });Use for:
- Stateful objects
- Objects that should not be shared
- Lightweight objects
Example:
class UniqueIdGenerator { private id: string;
constructor() { this.id = crypto.randomUUID(); }
getId() { return this.id; }}
// Register as transientcontainer.register(UniqueIdGenerator, { scope: 'transient' });Registering Dependencies
Section titled “Registering Dependencies”Class Registration
Section titled “Class Registration”class MyService { doSomething() { return 'done'; }}
container.register(MyService, { scope: 'singleton' });Factory Registration
Section titled “Factory Registration”Use a factory function for complex initialization:
class DatabaseService { constructor(private connectionString: string) {}
async query(sql: string) { // Use connectionString return []; }}
container.register(DatabaseService, { scope: 'singleton', factory: () => { const connectionString = process.env.DATABASE_URL || 'default'; return new DatabaseService(connectionString); },});Async Factory
Section titled “Async Factory”container.register(DatabaseService, { scope: 'singleton', factory: async () => { const config = await loadConfig(); const db = new DatabaseService(config.dbUrl); await db.connect(); return db; },});Advanced Patterns
Section titled “Advanced Patterns”Nested Dependencies
Section titled “Nested Dependencies”Services can depend on other services:
class DatabaseService { async query(sql: string) { return []; }}
class UserRepository { constructor(private db: DatabaseService) {}
async findAll() { return this.db.query('SELECT * FROM users'); }
async findById(id: string) { return this.db.query(`SELECT * FROM users WHERE id = ${id}`); }}
class UserService { constructor(private userRepo: UserRepository) {}
async getUsers() { return this.userRepo.findAll(); }
async getUser(id: string) { return this.userRepo.findById(id); }}
// Register all servicescontainer.register(DatabaseService, { scope: 'singleton' });container.register(UserRepository, { scope: 'singleton' });container.register(UserService, { scope: 'request' });
// Use in controller@Controller('/users')class UserController { @Get('/') async list(@Depends(UserService) userService: UserService) { // UserService automatically gets UserRepository // UserRepository automatically gets DatabaseService return await userService.getUsers(); }}Multiple Dependencies
Section titled “Multiple Dependencies”Inject multiple dependencies in a single handler:
@Post('/users')async createUser( @Body(UserSchema) userData: User, @Depends(UserService) userService: UserService, @Depends(EmailService) emailService: EmailService, @Depends(LoggerService) logger: LoggerService) { logger.info('Creating user', userData);
const user = await userService.createUser(userData);
await emailService.sendWelcomeEmail(user.email);
logger.info('User created', user);
return user;}Conditional Dependencies
Section titled “Conditional Dependencies”Use factory functions for conditional logic:
container.register(CacheService, { scope: 'singleton', factory: () => { if (process.env.REDIS_URL) { return new RedisCacheService(process.env.REDIS_URL); } else { return new InMemoryCacheService(); } },});Interface-Based Dependencies
Section titled “Interface-Based Dependencies”interface IEmailService { sendEmail(to: string, subject: string, body: string): Promise<void>;}
class SendGridEmailService implements IEmailService { async sendEmail(to: string, subject: string, body: string) { // SendGrid implementation }}
class MockEmailService implements IEmailService { async sendEmail(to: string, subject: string, body: string) { console.log(`Mock email to ${to}: ${subject}`); }}
// Register based on environmentcontainer.register(IEmailService as any, { scope: 'singleton', factory: () => { if (process.env.NODE_ENV === 'production') { return new SendGridEmailService(); } else { return new MockEmailService(); } },});Testing with DI
Section titled “Testing with DI”Mocking Dependencies
Section titled “Mocking Dependencies”import { describe, it, expect, beforeEach } from 'bun:test';import { createTestApp, mockDependency } from 'veloce/testing';
describe('UserController', () => { let app: Veloce; let mockUserService: any;
beforeEach(() => { app = createTestApp();
// Create mock mockUserService = { getUsers: async () => [ { id: 1, name: 'Test User' }, ], getUserById: async (id: string) => { return { id: parseInt(id), name: 'Test User' }; }, };
// Replace real service with mock mockDependency(UserService, mockUserService);
app.include(UserController); });
it('should get users', async () => { const response = await app.request('/users'); const data = await response.json();
expect(data).toEqual([ { id: 1, name: 'Test User' }, ]); });
it('should get user by id', async () => { const response = await app.request('/users/1'); const data = await response.json();
expect(data).toEqual({ id: 1, name: 'Test User', }); });});Test-Specific Container
Section titled “Test-Specific Container”describe('UserService', () => { it('should create user', async () => { const app = createTestApp(); const container = app.getContainer();
// Register test database const testDb = new InMemoryDatabase(); container.register(DatabaseService, { scope: 'singleton', factory: () => testDb, });
container.register(UserService, { scope: 'singleton' });
const userService = await container.resolve(UserService); const user = await userService.createUser({ name: 'Test', email: 'test@example.com', });
expect(user.name).toBe('Test'); });});Best Practices
Section titled “Best Practices”1. Use Constructor Injection for Services
Section titled “1. Use Constructor Injection for Services”// ✓ Goodclass UserService { constructor( private db: DatabaseService, private cache: CacheService ) {}
async getUsers() { const cached = await this.cache.get('users'); if (cached) return cached;
const users = await this.db.query('SELECT * FROM users'); await this.cache.set('users', users); return users; }}
// ✗ Bad - Direct instantiationclass UserService { private db = new DatabaseService(); private cache = new CacheService();
async getUsers() { // ... }}2. Choose Appropriate Scopes
Section titled “2. Choose Appropriate Scopes”// ✓ Good - Stateless services as singletoncontainer.register(ConfigService, { scope: 'singleton' });container.register(DatabaseService, { scope: 'singleton' });
// ✓ Good - Request-specific services as request-scopedcontainer.register(AuthService, { scope: 'request' });container.register(RequestLogger, { scope: 'request' });
// ✓ Good - Stateful objects as transientcontainer.register(FormValidator, { scope: 'transient' });3. Avoid Circular Dependencies
Section titled “3. Avoid Circular Dependencies”// ✗ Bad - Circular dependencyclass ServiceA { constructor(private serviceB: ServiceB) {}}
class ServiceB { constructor(private serviceA: ServiceA) {}}
// ✓ Good - Extract shared logicclass SharedService { doSomething() {}}
class ServiceA { constructor(private shared: SharedService) {}}
class ServiceB { constructor(private shared: SharedService) {}}4. Keep Services Focused
Section titled “4. Keep Services Focused”// ✓ Good - Single responsibilityclass UserRepository { async findAll() {} async findById(id: string) {} async create(user: User) {} async update(id: string, user: User) {} async delete(id: string) {}}
class UserService { constructor(private userRepo: UserRepository) {}
async getUsers() { return this.userRepo.findAll(); }
async validateAndCreateUser(userData: any) { // Validation logic return this.userRepo.create(userData); }}
// ✗ Bad - Mixed concernsclass UserService { async getUsers() {} async createUser() {} async sendEmail() {} async logActivity() {} async generateReport() {}}5. Use Interfaces for Flexibility
Section titled “5. Use Interfaces for Flexibility”// ✓ Goodinterface IStorageService { save(key: string, value: any): Promise<void>; get(key: string): Promise<any>;}
class S3StorageService implements IStorageService { async save(key: string, value: any) { // S3 implementation }
async get(key: string) { // S3 implementation }}
class LocalStorageService implements IStorageService { async save(key: string, value: any) { // Local implementation }
async get(key: string) { // Local implementation }}
// Easy to swap implementationscontainer.register(IStorageService as any, { factory: () => { return process.env.USE_S3 ? new S3StorageService() : new LocalStorageService(); },});Common Patterns
Section titled “Common Patterns”Repository Pattern
Section titled “Repository Pattern”class UserRepository { constructor(private db: DatabaseService) {}
async findAll(): Promise<User[]> { return this.db.query('SELECT * FROM users'); }
async findById(id: string): Promise<User | null> { const users = await this.db.query(`SELECT * FROM users WHERE id = ${id}`); return users[0] || null; }
async create(user: Omit<User, 'id'>): Promise<User> { return this.db.query('INSERT INTO users ...', user); }}
container.register(UserRepository, { scope: 'singleton' });Service Layer Pattern
Section titled “Service Layer Pattern”class UserService { constructor( private userRepo: UserRepository, private emailService: EmailService, private logger: LoggerService ) {}
async createUser(userData: CreateUserDto): Promise<User> { this.logger.info('Creating user', userData);
const user = await this.userRepo.create(userData);
await this.emailService.sendWelcomeEmail(user.email);
this.logger.info('User created', user);
return user; }}
container.register(UserService, { scope: 'request' });Factory Pattern
Section titled “Factory Pattern”class EmailServiceFactory { create(type: 'sendgrid' | 'ses' | 'mock'): IEmailService { switch (type) { case 'sendgrid': return new SendGridEmailService(); case 'ses': return new SESEmailService(); case 'mock': return new MockEmailService(); } }}
container.register(EmailServiceFactory, { scope: 'singleton' });Next Steps
Section titled “Next Steps”- Learn about Plugins
- Read the Testing Guide
- Check out the Getting Started Guide