HazelJS Modules
HazelJS 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. HazelJS Modules group related controllers, providers, and other modules into cohesive blocks of functionality using the @HazelModule() decorator.
Quick Reference
- Purpose: HazelJS Modules organize the application into cohesive feature groups. Each module declares its controllers, providers, imports, and exports.
- When to use: Use a HazelJS Module for every feature area (e.g., UserModule, AuthModule, ChatModule). Every HazelJS application requires one root module (AppModule).
- Key concepts:
@HazelModule(options)decorator, root module, feature modules,imports(other modules),controllers(route handlers),providers(services),exports(shared providers), dynamic modules (forRoot,forFeature). - Inputs: Module configuration object with
imports,controllers,providers,exports. - Outputs: An organized module that groups related functionality and registers it with the HazelJS DI container.
- Dependencies:
@hazeljs/core. - Common patterns: Create feature module with
@HazelModule({ controllers: [...], providers: [...] })→ import into root module'simportsarray → use dynamic modules (AIModule.register(),RagModule.forRoot()) for configurable packages. - Common mistakes: Forgetting to import a feature module in the root module; forgetting to export a provider that other modules need; circular module imports; registering a controller in multiple modules.
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