Modules
Modules are the fundamental organizational unit in HazelJS. Every HazelJS application has at least one module — the root module — which serves as the entry point. Modules group related controllers, providers, and other modules into cohesive blocks of functionality.
Purpose
Modules solve the problem of organizing a growing application. As your codebase expands, you need a way to:
- Group Related Code: Keep controllers and services that work together in the same module
- Encapsulate Logic: Hide internal implementation details and only expose what's needed
- Share Functionality: Reuse modules across different parts of your application
- Manage Dependencies: Control which providers are available where
Architecture
Modules form a tree structure where the root module imports feature modules, and feature modules can import other modules:
AppModule (root)
├── UserModule
│ ├── UserController
│ └── UserService
├── AuthModule
│ ├── AuthController
│ ├── AuthService
│ └── JwtService
└── ProductModule
├── ProductController
└── ProductService
The @HazelModule Decorator
The @HazelModule() decorator is a class decorator that marks a class as a module and accepts a metadata object describing how the module should be composed.
Configuration Options:
interface ModuleOptions {
imports?: Type[]; // Other modules this module depends on
controllers?: Type[]; // Controllers that belong to this module
providers?: Type[]; // Services/providers to register in this module
exports?: Type[]; // Providers to make available to importing modules
}
Basic Module
import { HazelModule } from '@hazeljs/core';
import { UserController } from './user.controller';
import { UserService } from './user.service';
@HazelModule({
controllers: [UserController],
providers: [UserService],
})
export class UserModule {}
How it works:
controllers— registersUserControllerso its routes are handledproviders— registersUserServicein the DI container so it can be injected intoUserController
The Root Module
Every application has exactly one root module. This is the module you pass to HazelApp:
// app.module.ts
import { HazelModule } from '@hazeljs/core';
import { UserModule } from './user/user.module';
import { AuthModule } from './auth/auth.module';
@HazelModule({
imports: [UserModule, AuthModule],
})
export class AppModule {}
// main.ts
import { HazelApp } from '@hazeljs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = new HazelApp(AppModule);
await app.listen(3000);
}
bootstrap();
The root module typically doesn't have its own controllers or providers — it just imports feature modules.
Feature Modules
Feature modules group related functionality. Each feature module encapsulates a specific domain:
// user/user.module.ts
import { HazelModule } from '@hazeljs/core';
import { UserController } from './user.controller';
import { UserService } from './user.service';
import { UserRepository } from './user.repository';
@HazelModule({
controllers: [UserController],
providers: [UserService, UserRepository],
exports: [UserService], // Make UserService available to other modules
})
export class UserModule {}
// user/user.controller.ts
import { Controller, Get, Post, Body, Param } from '@hazeljs/core';
import { UserService } from './user.service';
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}
@Get()
findAll() {
return this.userService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.userService.findOne(parseInt(id));
}
@Post()
create(@Body() createUserDto: { name: string; email: string }) {
return this.userService.create(createUserDto);
}
}
// user/user.service.ts
import { Service } from '@hazeljs/core';
@Service()
export class UserService {
private users = [
{ id: 1, name: 'John Doe', email: 'john@example.com' },
];
findAll() {
return this.users;
}
findOne(id: number) {
return this.users.find(u => u.id === id);
}
create(user: { name: string; email: string }) {
const newUser = { id: this.users.length + 1, ...user };
this.users.push(newUser);
return newUser;
}
}
Imports and Exports
Importing Modules
When you import a module, all of its exported providers become available for injection in the importing module:
// order/order.module.ts
import { HazelModule } from '@hazeljs/core';
import { OrderController } from './order.controller';
import { OrderService } from './order.service';
import { UserModule } from '../user/user.module';
@HazelModule({
imports: [UserModule], // Import UserModule
controllers: [OrderController],
providers: [OrderService],
})
export class OrderModule {}
// order/order.service.ts
import { Service } from '@hazeljs/core';
import { UserService } from '../user/user.service';
@Service()
export class OrderService {
// UserService is available because UserModule exports it
constructor(private readonly userService: UserService) {}
async createOrder(userId: number, items: string[]) {
const user = this.userService.findOne(userId);
if (!user) {
throw new Error('User not found');
}
return { userId, items, createdAt: new Date() };
}
}
Exporting Providers
Use exports to control which providers are visible to importing modules:
@HazelModule({
providers: [UserService, UserRepository, UserHelper],
exports: [UserService], // Only UserService is available outside this module
})
export class UserModule {}
UserService— accessible to any module that importsUserModuleUserRepositoryandUserHelper— internal toUserModule, not accessible outside
Re-exporting Modules
You can re-export an imported module to make its exports available transitively:
@HazelModule({
imports: [DatabaseModule],
exports: [DatabaseModule], // Re-export so importers also get DatabaseModule's exports
})
export class SharedModule {}
Dynamic Modules
Some modules need to be configured differently depending on the context. Dynamic modules use static methods like forRoot() or forFeature() to accept configuration:
ConfigModule Example
import { HazelModule } from '@hazeljs/core';
@HazelModule({
imports: [
ConfigModule.forRoot({
envFilePath: '.env',
isGlobal: true,
}),
],
})
export class AppModule {}
Creating a Dynamic Module
import { HazelModule } from '@hazeljs/core';
export interface DatabaseModuleOptions {
host: string;
port: number;
database: string;
}
export class DatabaseModule {
static forRoot(options: DatabaseModuleOptions) {
return {
module: DatabaseModule,
providers: [
{
provide: 'DATABASE_OPTIONS',
useValue: options,
},
DatabaseService,
],
exports: [DatabaseService],
};
}
}
Usage:
@HazelModule({
imports: [
DatabaseModule.forRoot({
host: 'localhost',
port: 5432,
database: 'myapp',
}),
],
})
export class AppModule {}
forRoot vs forFeature
A common pattern is to use two static methods:
forRoot()— called once in the root module, sets up global configurationforFeature()— called in feature modules, registers feature-specific providers
export class CacheModule {
// Called once in AppModule
static forRoot(options: { redis: string }) {
return {
module: CacheModule,
providers: [
{ provide: 'CACHE_OPTIONS', useValue: options },
CacheService,
],
exports: [CacheService],
};
}
// Called in feature modules
static forFeature(prefix: string) {
return {
module: CacheModule,
providers: [
{ provide: 'CACHE_PREFIX', useValue: prefix },
],
};
}
}
Module Alias
@Module() is an alias for @HazelModule(). Both work identically:
import { Module } from '@hazeljs/core';
@Module({
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Complete Example
Here's a full multi-module application:
// database/database.module.ts
import { HazelModule, Service } from '@hazeljs/core';
@Service()
export class DatabaseService {
query(sql: string) {
console.log(`Executing: ${sql}`);
return [];
}
}
@HazelModule({
providers: [DatabaseService],
exports: [DatabaseService],
})
export class DatabaseModule {}
// user/user.service.ts
import { Service } from '@hazeljs/core';
import { DatabaseService } from '../database/database.module';
@Service()
export class UserService {
constructor(private readonly db: DatabaseService) {}
findAll() {
return this.db.query('SELECT * FROM users');
}
}
// user/user.controller.ts
import { Controller, Get } from '@hazeljs/core';
import { UserService } from './user.service';
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}
@Get()
findAll() {
return this.userService.findAll();
}
}
// user/user.module.ts
import { HazelModule } from '@hazeljs/core';
import { UserController } from './user.controller';
import { UserService } from './user.service';
import { DatabaseModule } from '../database/database.module';
@HazelModule({
imports: [DatabaseModule],
controllers: [UserController],
providers: [UserService],
exports: [UserService],
})
export class UserModule {}
// app.module.ts
import { HazelModule } from '@hazeljs/core';
import { UserModule } from './user/user.module';
@HazelModule({
imports: [UserModule],
})
export class AppModule {}
// main.ts
import { HazelApp } from '@hazeljs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = new HazelApp(AppModule);
await app.listen(3000);
console.log('Application running on http://localhost:3000');
}
bootstrap();
Best Practices
-
One Module Per Feature: Create a module for each feature domain (users, orders, auth, etc.)
-
Keep Modules Focused: Each module should have a single responsibility
-
Export Only What's Needed: Only export providers that other modules need — keep internal services private
-
Use Dynamic Modules for Configuration: When a module needs different config in different contexts, use
forRoot()/forFeature() -
Avoid Circular Imports: If Module A imports Module B and Module B imports Module A, refactor shared logic into a third module
-
Root Module Should Be Thin: The root
AppModuleshould primarily import feature modules, not define its own controllers/providers
What's Next?
- Learn about Providers for dependency injection
- Add Guards for authentication
- Implement Middleware for cross-cutting concerns
- Use Config for type-safe configuration