Skip to content

Modules

Modules are the primary way you organize a Stratal application. A module groups related controllers, providers, and other modules into a cohesive unit. Every Stratal application has at least one module: the root module.

The @Module decorator marks a class as a Stratal module and accepts a ModuleOptions object:

import { Module } from 'stratal/module'
@Module({
imports: [],
providers: [],
controllers: [],
consumers: [],
jobs: [],
})
export class AppModule {}

Each property is optional:

PropertyDescription
importsOther modules (or dynamic modules) whose providers this module needs
providersServices and other injectable classes to register in the DI container
controllersController classes that handle HTTP requests
consumersQueue consumer classes
jobsCron job classes

The root module is the entry point of your module tree. You pass it to the Stratal constructor:

import { Stratal } from 'stratal'
import { AppModule } from './app.module'
export default new Stratal({ module: AppModule })

There is nothing structurally special about the root module. It is simply the module you pass to Stratal. The framework walks its imports array recursively to discover and initialize every module in the application.

As your application grows, you should split related functionality into feature modules. Each feature module encapsulates its own controllers and providers, then gets imported into the root module (or another feature module).

import { Module } from 'stratal/module'
import { UsersController } from './users.controller'
import { UsersService } from './users.service'
@Module({
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}

Import it into your root module:

import { Module } from 'stratal/module'
import { UsersModule } from './users/users.module'
@Module({
imports: [UsersModule],
})
export class AppModule {}

Sometimes a module needs configuration before it can be used. Dynamic modules solve this by exposing static methods that return a DynamicModule object.

Use forRoot() when you have static configuration values available at import time:

import { Module, DynamicModule } from 'stratal/module'
@Module({})
export class CacheModule {
static forRoot(options: { ttl: number }): DynamicModule {
return {
module: CacheModule,
providers: [
{ provide: CACHE_OPTIONS, useValue: options },
CacheService,
],
}
}
}

Import it with configuration:

@Module({
imports: [CacheModule.forRoot({ ttl: 3600 })],
})
export class AppModule {}

Asynchronous configuration with forRootAsync()

Section titled “Asynchronous configuration with forRootAsync()”

Use forRootAsync() when the configuration depends on other services that need to be resolved first:

import { Module, DynamicModule, AsyncModuleOptions } from 'stratal/module'
@Module({})
export class DatabaseModule {
static forRootAsync<TOptions>(
options: AsyncModuleOptions<TOptions>,
): DynamicModule {
return {
module: DatabaseModule,
providers: [
{
provide: DB_OPTIONS,
useFactory: options.useFactory,
inject: options.inject,
},
DatabaseService,
],
}
}
}

Import it with an async factory:

@Module({
imports: [
DatabaseModule.forRootAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
connectionString: config.get('DATABASE_URL'),
}),
}),
],
})
export class AppModule {}

The inject array tells Stratal which dependencies to resolve and pass to useFactory.

Modules can configure middleware by implementing the MiddlewareConfigurable interface. Define a configure() method that receives a MiddlewareConsumer:

import { Module } from 'stratal/module'
import { MiddlewareConfigurable, MiddlewareConsumer } from 'stratal/middleware'
import { LoggingMiddleware } from './logging.middleware'
@Module({
controllers: [UsersController],
})
export class UsersModule implements MiddlewareConfigurable {
configure(consumer: MiddlewareConsumer) {
consumer.apply(LoggingMiddleware).forRoutes('/api/users/*')
}
}

For a complete guide on writing and applying middleware, see the Middleware guide.