Skip to main content
nestrs ships a synchronous, type-safe dependency injection container built on Rust’s TypeId system. When you call NestFactory::create::<AppModule>(), the framework traverses the module graph, builds a ProviderRegistry that maps each registered type to a factory, and injects dependencies by resolving Arc<T> fields from that registry. No runtime reflection, no string keys — every injection site is checked at compile time.

How the container works at runtime

ProviderRegistry is the heart of the DI system. It stores one entry per provider type, keyed by TypeId. When a type is requested, the registry calls the factory (the generated Injectable::construct), caches the result if the scope is Singleton, and returns an Arc<T>.
// From nestrs-core/src/lib.rs
pub struct ProviderRegistry {
    entries: HashMap<TypeId, ProviderEntry>,
}

impl ProviderRegistry {
    pub fn get<T>(&self) -> Arc<T>
    where
        T: Send + Sync + 'static,
    { /* resolve, construct, downcast */ }
}
The Module trait’s build() method returns a (ProviderRegistry, Router) pair. NestFactory composes these pairs from the entire module graph into a single registry and a merged Axum router, then wraps the registry in an Arc that is injected as Axum State.

The three provider scopes

ScopeProviderScope variantBehaviorNestJS equivalent
SingletonProviderScope::SingletonOne shared Arc<T> per application; constructed once, cached in a OnceLockScope.DEFAULT
TransientProviderScope::TransientA fresh Arc<T> is constructed on every registry.get::<T>() callScope.TRANSIENT
RequestProviderScope::RequestOne Arc<T> per HTTP request, stored in a task-local cacheScope.REQUEST
Set the scope with the #[injectable] attribute:
use nestrs::prelude::*;

#[injectable]
pub struct SingletonService; // default

#[injectable(scope = "transient")]
pub struct TransientService;

#[injectable(scope = "request")]
pub struct PerRequestService;

ModuleRef — dynamic resolution after build

ModuleRef is a thin handle to the root ProviderRegistry after the module graph is constructed. Use it when you need to resolve providers dynamically — for example, in plugins or conditional code — without injecting them as struct fields:
use nestrs::core::{DiscoveryService, ModuleRef};

// Obtain before calling into_router():
// let mref: ModuleRef = app.module_ref();

let discovery = DiscoveryService::new(mref);
let _provider_ids = discovery.get_providers();
let _route_specs = discovery.get_routes(); // useful for OpenAPI / diagnostics
ModuleRef::get::<T>() follows the same scope rules as the container: singletons return the cached instance, transients produce a new one.
// nestrs-core/src/module_ref.rs
impl ModuleRef {
    pub fn get<T: Send + Sync + 'static>(&self) -> Arc<T> {
        self.registry.get()
    }
}

DiscoveryService

DiscoveryService exposes two introspection surfaces:
  • get_providers()Vec<TypeId> of every registered provider
  • get_provider_type_names() — debug-friendly Vec<&'static str> type names
  • get_routes() — all HTTP routes from the global RouteRegistry (useful for OpenAPI generation and diagnostics)
use nestrs::core::{DiscoveryService, ModuleRef};

let discovery = DiscoveryService::new(mref);
for name in discovery.get_provider_type_names() {
    println!("registered: {name}");
}
for route in discovery.get_routes() {
    println!("{} {}", route.method, route.path);
}
nestrs discovery is TypeId- and route-list-oriented. Unlike NestJS, there is no reflection over arbitrary metadata attached to class decorators.

NestJS DI concept mapping

NestJS conceptnestrs equivalent
@Injectable()#[injectable]
@Module({ providers })#[module(providers = [...])]
Constructor injectionArc<T> fields resolved by Injectable::construct
Scope.DEFAULTProviderScope::Singleton (default)
Scope.TRANSIENTProviderScope::Transient
Scope.REQUESTProviderScope::Request
useValueregistry.register_use_value::<T>(Arc<T>)
useFactoryregistry.register_use_factory::<T>(scope, |reg| Arc<T>)
useClassregistry.register_use_class::<T>()
ModuleRefModuleRef from nestrs::core
DiscoveryServiceDiscoveryService from nestrs::core
forwardRefforward_ref::<T>() in #[module(imports = [...])]
ConfigModule.forRootConfigurableModuleBuilder::for_root::<M>(options)
ConfigModule.forRootAsyncConfigurableModuleBuilder::for_root_async::<M, _, _>(async_fn).await

Request-scoped providers

Request scope gives each HTTP request its own instance of a provider, isolated from other concurrent requests. This is useful for per-request caches, correlation IDs, or any state that must not leak between requests.

Enabling request scope

Call use_request_scope() on your NestApplication before listening:
NestFactory::create::<AppModule>()
    .use_request_scope()
    .listen_graceful(3000)
    .await;

Marking a provider as request-scoped

#[injectable(scope = "request")]
pub struct RequestContext {
    pub request_id: String,
}

Extracting request-scoped providers in handlers

Use the RequestScoped<T> Axum extractor:
use nestrs::prelude::*;
use std::sync::Arc;

#[routes(state = AppService)]
impl AppController {
    #[get("/trace")]
    pub async fn trace(
        State(service): State<Arc<AppService>>,
        RequestScoped(ctx): RequestScoped<RequestContext>,
    ) -> String {
        format!("request_id={}", ctx.request_id)
    }
}
nestrs stores the request-scoped cache in a tokio::task_local! variable. Every request gets a fresh, empty cache; the first call to registry.get::<T>() for a Request-scoped type within that task constructs and caches the instance for the lifetime of that request.
Resolving a Request-scoped provider outside a request context (for example, during eager_init_singletons) panics because no task-local cache exists. Always enable use_request_scope() on the application before using the RequestScoped extractor.

Circular provider dependencies

If two providers each depend on the other, nestrs detects the cycle during construction and panics:
Circular provider dependency detected: TypeA -> TypeB -> TypeA
Fixes:
  • Split the shared concern into a third type that both depend on.
  • Defer work to on_module_init so construct only wires Arcs without triggering the cycle.
  • Use register_use_factory with a closure that resolves one side lazily on first use.
There is no forwardRef for individual providers — cycles must be broken in code structure or initialization order.