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:

  1. controllers — registers UserController so its routes are handled
  2. providers — registers UserService in the DI container so it can be injected into UserController

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 imports UserModule
  • UserRepository and UserHelper — internal to UserModule, 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 configuration
  • forFeature() — 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

  1. One Module Per Feature: Create a module for each feature domain (users, orders, auth, etc.)

  2. Keep Modules Focused: Each module should have a single responsibility

  3. Export Only What's Needed: Only export providers that other modules need — keep internal services private

  4. Use Dynamic Modules for Configuration: When a module needs different config in different contexts, use forRoot() / forFeature()

  5. Avoid Circular Imports: If Module A imports Module B and Module B imports Module A, refactor shared logic into a third module

  6. Root Module Should Be Thin: The root AppModule should 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