Providers
Providers are a fundamental concept in HazelJS. Many of the basic HazelJS classes may be treated as a provider – services, repositories, factories, helpers, and so on. The main idea of a provider is that it can be injected as a dependency.
Purpose
Providers are the building blocks of your HazelJS application. They encapsulate business logic, data access, and reusable functionality. The Dependency Injection (DI) system automatically manages their lifecycle and provides them to classes that need them.
Key Benefits:
- Separation of Concerns: Business logic is separated from HTTP handling
- Testability: Easy to mock and test in isolation
- Reusability: Share functionality across multiple controllers
- Maintainability: Centralized logic makes code easier to maintain
Architecture
Providers in HazelJS use decorators and metadata to enable automatic dependency injection:
import { Injectable } from '@hazeljs/core';
@Injectable()
export class UsersService {
private users = [
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Smith' },
];
findAll() {
return this.users;
}
findOne(id: number) {
return this.users.find((user) => user.id === id);
}
}
Services
Let's start by creating a simple UsersService. This service will be responsible for data storage and retrieval, and is designed to be used by the UsersController.
import { Injectable } from '@hazeljs/core';
interface User {
id: number;
name: string;
email: string;
}
@Injectable()
export class UsersService {
private users: User[] = [];
create(user: Omit<User, 'id'>): User {
const newUser = {
id: this.users.length + 1,
...user,
};
this.users.push(newUser);
return newUser;
}
findAll(): User[] {
return this.users;
}
findOne(id: number): User | undefined {
return this.users.find((user) => user.id === id);
}
update(id: number, updates: Partial<User>): User | undefined {
const user = this.findOne(id);
if (user) {
Object.assign(user, updates);
}
return user;
}
remove(id: number): boolean {
const index = this.users.findIndex((user) => user.id === id);
if (index !== -1) {
this.users.splice(index, 1);
return true;
}
return false;
}
}
Dependency Injection
HazelJS is built around the strong design pattern commonly known as Dependency Injection (DI). DI is a design pattern where dependencies are provided to a class rather than the class creating them itself.
Understanding Dependency Injection
How it works:
- Registration: Providers are registered with the DI container using
@Injectable() - Resolution: When a class needs a dependency, the container resolves it
- Injection: Dependencies are injected via constructor parameters
- Lifecycle: The container manages the lifecycle of all providers
Benefits:
- Loose Coupling: Classes don't depend on concrete implementations
- Testability: Easy to replace dependencies with mocks
- Flexibility: Change implementations without modifying dependent classes
- Maintainability: Centralized dependency management
@Injectable Decorator
The @Injectable() decorator is a class decorator that marks a class as a provider that can be injected.
Purpose: Registers the class with the DI container and enables automatic injection.
How it works:
- Stores metadata about the class using reflection
- Registers the class in the DI container
- Enables constructor parameter injection
- Configures the provider's scope (singleton, transient, request)
// @Injectable() is a class decorator
@Injectable()
// This marks UsersService as a provider
// The DI container can now create and inject instances
export class UsersService {
// Class implementation
}
Now we can inject the UsersService into the UsersController:
import { Controller, Get, Post, Body, Param } from '@hazeljs/core';
import { UsersService } from './users.service';
@Controller('users')
export class UsersController {
constructor(private usersService: UsersService) {}
@Get()
findAll() {
return this.usersService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.usersService.findOne(parseInt(id));
}
@Post()
create(@Body() createUserDto: any) {
return this.usersService.create(createUserDto);
}
}
Scopes
Providers have a lifetime (also called "scope") that determines when instances are created and how long they live. The scope controls how many instances exist and when they're destroyed.
Understanding Scopes
Why scopes matter:
- Performance: Singleton scope reduces memory usage and initialization overhead
- State Management: Request scope ensures each request has isolated state
- Resource Management: Proper scoping prevents memory leaks and resource exhaustion
Scope Types:
- Singleton (default): One instance for the entire application
- Transient: New instance for each injection
- Request: New instance for each HTTP request
Singleton Scope (Default)
A single instance of the provider is shared across the entire application. The instance lifetime is tied directly to the application lifecycle.
When to use:
- Stateless services (most common)
- Services that manage shared resources
- Services that are expensive to create
- Configuration services
How it works:
- Instance created once when first needed
- Same instance reused for all injections
- Instance lives for the entire application lifetime
- Destroyed when application shuts down
import { Injectable, Scope } from '@hazeljs/core';
@Injectable({ scope: Scope.SINGLETON })
export class UsersService {
// This service will be created once and shared
}
Transient Scope
Transient providers are not shared across consumers. Each consumer that injects a transient provider will receive a new, dedicated instance.
When to use:
- Services that maintain per-consumer state
- Services that shouldn't share state between consumers
- Services that are lightweight to create
How it works:
- New instance created for each injection
- Each consumer gets its own instance
- Instances are garbage collected when no longer referenced
import { Injectable, Scope } from '@hazeljs/core';
@Injectable({ scope: Scope.TRANSIENT })
// Each class that injects LoggerService gets a new instance
export class LoggerService {
private requestId: string;
constructor() {
// Each instance has its own requestId
this.requestId = Math.random().toString(36);
}
log(message: string) {
console.log(`[${this.requestId}] ${message}`);
}
}
// Usage:
@Controller('users')
export class UsersController {
constructor(
private logger1: LoggerService, // New instance
private logger2: LoggerService, // Another new instance
) {
// logger1 and logger2 are different instances
}
}
Request Scope
Request-scoped providers are created for each incoming HTTP request and garbage collected after the request has completed processing.
When to use:
- Services that need request-specific state
- Services that store user context
- Services that should be isolated per request
How it works:
- New instance created at the start of each HTTP request
- Same instance reused within the same request
- Instance destroyed after request completes
- Not available outside of request context
import { Injectable, Scope } from '@hazeljs/core';
@Injectable({ scope: Scope.REQUEST })
// New instance for each HTTP request
export class RequestContextService {
private userId: string | null = null;
private requestId: string;
constructor() {
// Each request gets a unique request ID
this.requestId = Math.random().toString(36);
}
setUserId(userId: string) {
this.userId = userId;
}
getUserId(): string | null {
return this.userId;
}
getRequestId(): string {
return this.requestId;
}
}
// Usage in controller:
@Controller('users')
export class UsersController {
constructor(
private requestContext: RequestContextService, // Request-scoped
) {}
@Get('profile')
getProfile() {
// requestContext is specific to this HTTP request
const userId = this.requestContext.getUserId();
return { userId, requestId: this.requestContext.getRequestId() };
}
}
Important Notes:
- Request-scoped providers can only be injected in request context (controllers, guards, interceptors)
- Cannot be injected in singleton services (would cause errors)
- Use for request-specific data like user context, request IDs, etc.
Custom Providers
There are several ways to create custom providers.
Value Providers
The useValue syntax is useful for injecting a constant value:
const configProvider = {
provide: 'CONFIG',
useValue: {
apiUrl: 'https://api.example.com',
timeout: 5000,
},
};
@HazelModule({
providers: [configProvider],
})
export class AppModule {}
Inject it using @Inject():
import { Injectable, Inject } from '@hazeljs/core';
@Injectable()
export class ApiService {
constructor(@Inject('CONFIG') private config: any) {
console.log(this.config.apiUrl);
}
}
Class Providers
The useClass syntax allows you to dynamically determine a class:
const loggerProvider = {
provide: LoggerService,
useClass:
process.env.NODE_ENV === 'development'
? DevLoggerService
: ProdLoggerService,
};
Factory Providers
The useFactory syntax allows for creating providers dynamically:
const databaseProvider = {
provide: 'DATABASE_CONNECTION',
useFactory: async () => {
const connection = await createConnection({
type: 'postgres',
host: 'localhost',
port: 5432,
});
return connection;
},
};
With dependencies:
const repositoryProvider = {
provide: 'USER_REPOSITORY',
useFactory: (connection: Connection) => {
return connection.getRepository(User);
},
inject: ['DATABASE_CONNECTION'],
};
Optional Providers
Occasionally, you might have dependencies which do not necessarily have to be resolved. For instance, your class may depend on a configuration object, but if none is passed, the default values should be used.
import { Injectable, Optional, Inject } from '@hazeljs/core';
@Injectable()
export class HttpService {
constructor(@Optional() @Inject('HTTP_OPTIONS') private options: any) {
this.options = options || { timeout: 3000 };
}
}
Property-based Injection
In some very specific cases, property-based injection might be useful. For instance, if your top-level class depends on either one or multiple providers, passing them all the way up by calling super() in sub-classes can be very tedious.
import { Injectable, Inject } from '@hazeljs/core';
@Injectable()
export class BaseService {
@Inject()
protected logger: LoggerService;
}
@Injectable()
export class UsersService extends BaseService {
// logger is automatically injected from parent
findAll() {
this.logger.log('Finding all users');
return [];
}
}
Complete Example
Here's a complete example with a service, controller, and module:
import { Injectable } from '@hazeljs/core';
interface User {
id: number;
name: string;
email: string;
}
@Injectable()
export class UsersService {
private users: User[] = [
{ id: 1, name: 'John Doe', email: 'john@example.com' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com' },
];
findAll(): User[] {
return this.users;
}
findOne(id: number): User | undefined {
return this.users.find((user) => user.id === id);
}
create(user: Omit<User, 'id'>): User {
const newUser = {
id: Math.max(...this.users.map((u) => u.id), 0) + 1,
...user,
};
this.users.push(newUser);
return newUser;
}
}
import { Controller, Get, Post, Param, Body } from '@hazeljs/core';
import { UsersService } from './users.service';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get()
findAll() {
return this.usersService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.usersService.findOne(parseInt(id));
}
@Post()
create(@Body() createUserDto: { name: string; email: string }) {
return this.usersService.create(createUserDto);
}
}
import { HazelModule } from '@hazeljs/core';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
@HazelModule({
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}
Try It Yourself
- Create a new service:
# Create users.service.ts with the code above
- Create a controller that uses it:
# Create users.controller.ts with the code above
- Register both in a module:
# Create users.module.ts with the code above
- Import the module in your app:
import { HazelModule } from '@hazeljs/core';
import { UsersModule } from './users/users.module';
@HazelModule({
imports: [UsersModule],
})
export class AppModule {}
- Test the endpoints:
# GET all users
curl http://localhost:3000/users
# GET one user
curl http://localhost:3000/users/1
# POST create user
curl -X POST http://localhost:3000/users \
-H "Content-Type: application/json" \
-d '{"name":"Bob","email":"bob@example.com"}'
What's Next?
- Learn about Modules to organize your providers
- Add Database integration with Prisma
- Implement Caching for better performance
- Use Testing to test your services