Providers
Create injectable services with dependency injection
What are Providers?
Providers are classes that can be injected as dependencies. They encapsulate business logic, data access, and other reusable functionality. In Dwex, any class marked with @Injectable() is a provider.
Basic Provider
import { Injectable } from "@dwex/core";
@Injectable()
export class UserService {
private users = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
];
findAll() {
return this.users;
}
findOne(id: number) {
return this.users.find((user) => user.id === id);
}
}Registering Providers
Register providers in a module's providers array:
import { Module } from "@dwex/core";
import { UserController } from "./user.controller";
import { UserService } from "./user.service";
@Module({
controllers: [UserController],
providers: [UserService],
})
export class UserModule {}Dependency Injection
Inject providers into controllers or other providers:
import { Controller, Get, Injectable } from "@dwex/core";
@Injectable()
export class UserService {
findAll() {
return ["Alice", "Bob"];
}
}
@Controller("users")
export class UserController {
constructor(private readonly userService: UserService) {}
@Get()
findAll() {
return this.userService.findAll();
}
}Dwex automatically resolves and injects UserService when creating UserController.
Constructor Injection
The most common form of DI in Dwex:
@Injectable()
export class PostService {
constructor(
private readonly userService: UserService,
private readonly db: DatabaseService,
private readonly logger: Logger
) {}
async createPost(userId: string, title: string) {
this.logger.log(`Creating post for user ${userId}`);
const user = await this.userService.findOne(userId);
return await this.db.posts.create({ userId, title });
}
}Service Layers
Organize your code with layered services:
// Repository layer - data access
@Injectable()
export class UserRepository {
constructor(private readonly db: DatabaseService) {}
async findAll() {
return await this.db.users.findMany();
}
async create(data: CreateUserDto) {
return await this.db.users.create(data);
}
}
// Service layer - business logic
@Injectable()
export class UserService {
constructor(
private readonly userRepository: UserRepository,
private readonly emailService: EmailService
) {}
async createUser(data: CreateUserDto) {
const user = await this.userRepository.create(data);
await this.emailService.sendWelcome(user.email);
return user;
}
}
// Controller layer - HTTP handling
@Controller("users")
export class UserController {
constructor(private readonly userService: UserService) {}
@Post()
async create(@Body() data: CreateUserDto) {
return await this.userService.createUser(data);
}
}Provider Scopes
Control the lifecycle of providers:
import { Injectable, Scope } from "@dwex/core";
// Singleton (default) - one instance for entire app
@Injectable()
export class ConfigService {}
// Request-scoped - new instance per request
@Injectable({ scope: Scope.REQUEST })
export class RequestContextService {
private data = new Map();
set(key: string, value: any) {
this.data.set(key, value);
}
get(key: string) {
return this.data.get(key);
}
}
// Transient - new instance every time it's injected
@Injectable({ scope: Scope.TRANSIENT })
export class TemporaryService {}Best Practice: Use Scope.SINGLETON (default) unless you specifically need per-request or transient instances.
Exporting Providers
Make providers available to other modules:
@Module({
providers: [UserService, UserRepository],
exports: [UserService], // UserRepository is private
})
export class UserModule {}Other modules can now import and use UserService:
@Module({
imports: [UserModule],
controllers: [PostController],
providers: [PostService],
})
export class PostModule {}@Injectable()
export class PostService {
constructor(private readonly userService: UserService) {}
// Can now use UserService
}Optional Dependencies
Mark dependencies as optional with @Optional():
import { Injectable, Optional } from "@dwex/core";
@Injectable()
export class NotificationService {
constructor(
@Optional() private readonly smsService?: SmsService,
private readonly emailService: EmailService
) {}
async notify(user: User, message: string) {
// Always send email
await this.emailService.send(user.email, message);
// Send SMS only if service is available
if (this.smsService) {
await this.smsService.send(user.phone, message);
}
}
}Custom Providers
Value Providers
Provide a static value:
@Module({
providers: [
{
provide: "API_KEY",
useValue: process.env.API_KEY,
},
{
provide: "CONFIG",
useValue: {
database: { host: "localhost" },
cache: { ttl: 3600 },
},
},
],
})
export class AppModule {}Inject with @Inject():
import { Injectable, Inject } from "@dwex/core";
@Injectable()
export class ApiService {
constructor(@Inject("API_KEY") private readonly apiKey: string) {}
async callApi() {
// Use this.apiKey
}
}Factory Providers
Create providers dynamically:
@Module({
providers: [
{
provide: "DATABASE_CONNECTION",
useFactory: async () => {
const connection = await createDatabaseConnection({
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT),
});
return connection;
},
},
],
})
export class DatabaseModule {}Factory with Dependencies
Factories can inject other providers:
@Module({
providers: [
ConfigService,
{
provide: "DATABASE_CONNECTION",
useFactory: async (config: ConfigService) => {
const dbConfig = config.get("database");
return await createConnection(dbConfig);
},
inject: [ConfigService],
},
],
})
export class DatabaseModule {}Circular Dependencies
Avoid circular dependencies when possible:
// Avoid this
@Injectable()
export class UserService {
constructor(private readonly postService: PostService) {}
}
@Injectable()
export class PostService {
constructor(private readonly userService: UserService) {}
}Solutions:
- Restructure: Extract common logic to a third service
- Forward Reference: Use lazy injection (if supported)
- Events: Use an event system instead of direct dependencies
Best Practices
1. Single Responsibility
Each service should have one clear purpose:
// Good - focused responsibilities
@Injectable()
export class UserService {
/* User operations */
}
@Injectable()
export class EmailService {
/* Email operations */
}
// Avoid - mixed concerns
@Injectable()
export class UserAndEmailService {
/* User and email operations */
}2. Use Interfaces
Define contracts for your services:
export interface IUserService {
findAll(): Promise<User[]>;
findOne(id: string): Promise<User>;
create(data: CreateUserDto): Promise<User>;
}
@Injectable()
export class UserService implements IUserService {
async findAll() {
/* ... */
}
async findOne(id: string) {
/* ... */
}
async create(data: CreateUserDto) {
/* ... */
}
}3. Organize by Domain
Group related services together:
src/modules/users/
├── user.controller.ts
├── user.service.ts
├── user.repository.ts
└── user.module.ts4. Use Constructor Injection
Prefer constructor injection over property injection:
// Good
@Injectable()
export class UserService {
constructor(private readonly db: DatabaseService) {}
}
// Avoid
@Injectable()
export class UserService {
@Inject()
private db: DatabaseService;
}5. Test-Friendly Design
Design services to be easily testable:
@Injectable()
export class UserService {
constructor(
private readonly repository: UserRepository,
private readonly logger: Logger
) {}
async createUser(data: CreateUserDto) {
this.logger.log("Creating user");
return await this.repository.create(data);
}
}Easy to mock dependencies in tests:
const mockRepository = { create: vi.fn() };
const mockLogger = { log: vi.fn() };
const service = new UserService(mockRepository, mockLogger);