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>.
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
Set the scope with the
#[injectable] attribute:
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:
ModuleRef::get::<T>() follows the same scope rules as the container: singletons return the cached instance, transients produce a new one.
DiscoveryService
DiscoveryService exposes two introspection surfaces:
get_providers()—Vec<TypeId>of every registered providerget_provider_type_names()— debug-friendlyVec<&'static str>type namesget_routes()— all HTTP routes from the globalRouteRegistry(useful for OpenAPI generation and diagnostics)
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
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
Calluse_request_scope() on your NestApplication before listening:
Marking a provider as request-scoped
Extracting request-scoped providers in handlers
Use theRequestScoped<T> Axum extractor:
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.
Circular provider dependencies
If two providers each depend on the other, nestrs detects the cycle during construction and panics:- Split the shared concern into a third type that both depend on.
- Defer work to
on_module_initsoconstructonly wiresArcs without triggering the cycle. - Use
register_use_factorywith a closure that resolves one side lazily on first use.
forwardRef for individual providers — cycles must be broken in code structure or initialization order.