Skip to main content
Modules are the fundamental unit of organization in nestrs. Every nestrs application has at least one root module, and larger applications divide their code into feature modules — each encapsulating a related set of controllers, providers, and imports. The #[module] proc-macro generates the Module and ModuleGraph trait implementations that NestFactory uses to build the provider registry and Axum router.

The #[module] macro

Annotate a plain struct with #[module] to define a module. The macro accepts four optional fields:
FieldTypeDescription
importsarray of module types or DynamicModule expressionsModules whose exported providers this module can use
controllersarray of controller typesAxum route sets registered under this module
providersarray of injectable typesServices available to controllers in this module
exportsarray of injectable typesProviders this module re-exports to importing modules
use nestrs::prelude::*;

#[module(
    imports = [DataModule],
    controllers = [AppController, AppControllerV2],
    providers = [AppService],
)]
pub struct AppModule;
Modules that re-export another module’s providers use re_exports:
#[module(
    imports = [PrismaModule],
    re_exports = [PrismaModule],
)]
pub struct DataModule;

Composing feature modules

1

Define a feature module

Create a module for a focused area of your application. List only the providers and controllers that belong to that feature.
use nestrs::prelude::*;

#[injectable]
pub struct UserService;

#[controller(prefix = "/users")]
pub struct UserController;

#[routes(state = UserService)]
impl UserController {
    #[get("/")]
    pub async fn list(State(svc): State<Arc<UserService>>) -> &'static str {
        "users"
    }
}

#[module(
    controllers = [UserController],
    providers  = [UserService],
    exports    = [UserService],
)]
pub struct UserModule;
2

Import the feature module from your root module

List the feature module in imports. Exported providers are absorbed into the importing module’s registry.
#[module(
    imports  = [UserModule],
    providers = [AppService],
)]
pub struct AppModule;
3

Bootstrap the application

Pass the root module to NestFactory::create. nestrs builds the full provider registry and Axum router automatically.
#[tokio::main]
async fn main() {
    NestFactory::create::<AppModule>()
        .set_global_prefix("api")
        .listen_graceful(3000)
        .await;
}

Dynamic modules

Static #[module] declarations cover most use cases. When you need to configure a module at runtime — loading options from environment variables, a remote secrets store, or feature flags — use a DynamicModule.

DynamicModule::from_module

Converts any static module into a DynamicModule at call time:
let dm = DynamicModule::from_module::<CacheModule>();

ConfigurableModuleBuilder

The ConfigurableModuleBuilder pattern mirrors NestJS’s forRoot / forRootAsync:
use nestrs::prelude::*;

#[derive(Clone)]
struct ApiOptions {
    base_url: String,
}

#[injectable]
struct HttpClientConfig {
    opts: Arc<ModuleOptions<ApiOptions, ConfigModule>>,
}

#[module(providers = [HttpClientConfig], exports = [HttpClientConfig])]
struct ConfigModule;

fn build() -> DynamicModule {
    ConfigurableModuleBuilder::<ApiOptions>::for_root::<ConfigModule>(ApiOptions {
        base_url: "https://api.example.com".into(),
    })
}

Lazy modules

DynamicModule::lazy::<M>() runs M::build() at most once per process and shares the singleton cells across all callers. This is useful for shared infrastructure modules (databases, caches) that multiple feature modules import independently.
let shared = DynamicModule::lazy::<DatabaseModule>();

Lifecycle sequence

When you call NestFactory::create and then listen or listen_graceful, nestrs drives the provider registry through the following sequence for all singleton providers:
1

eager_init_singletons()

Constructs every singleton in the registry synchronously. This is where circular provider dependency panics surface.
2

run_on_module_init().await

Calls on_module_init on every singleton. Use this hook for async setup (opening database pools, warming caches) that must complete before the server accepts traffic.
3

run_on_application_bootstrap().await

Calls on_application_bootstrap on every singleton. Use this for tasks that depend on all modules being fully initialized.
4

Server accepts requests

The Axum TCP listener opens and the application begins serving traffic.
5

Shutdown: run_on_application_shutdown().await

On SIGTERM or graceful shutdown, on_application_shutdown is called on every singleton.
6

Shutdown: run_on_module_destroy().await

on_module_destroy is called last. Use this to close connections and flush buffers.
Lifecycle hooks only run for singleton providers. Transient providers are constructed on demand and do not participate in the global hook sequence.

Circular module imports

If two modules import each other, nestrs detects the cycle and panics with:
Circular module dependency detected: A -> B -> ... -> A
Mark the back-edge import with forward_ref to break the cycle:
use nestrs::prelude::*;

#[module(
    imports = [BForwardModule],
    controllers = [AController],
    providers = [AState],
)]
struct AForwardModule;

#[module(
    imports = [forward_ref::<AForwardModule>()],
    controllers = [BController],
    providers = [BState],
)]
struct BForwardModule;
When possible, extract shared types into a third module and import that from both sides instead of introducing a circular reference.