Concepts
Providers / DI
Typed dependency injection, factories, optional dependencies, and request scope.
Dependency Injection
The Nidus container is keyed by Rust types, not strings.
let mut container = nidus_core::Container::new();
container.register_singleton(DatabasePool::new())?;
let pool = container.resolve::<DatabasePool>()?;
Factories can resolve dependencies through the same typed API:
container.register_singleton_factory(|container| {
Ok(UsersRepository::new(container.inject::<DatabasePool>()?))
})?;
Factory failures are reported with the provider type that failed and preserve the underlying source error.
Optional dependencies can be resolved without turning missing providers into startup failures:
let cache = container.optional::<CacheClient>()?;
if let Some(cache) = cache.as_ref() {
cache.warm();
}
Only missing providers become None; registered providers that fail to build still return their original construction error.
#[injectable] registers a singleton provider by default and recognizes Inject<T> and Optional<T> fields. Required dependencies use container.inject()?; optional dependencies use container.optional()?. Other fields are rejected at compile time so provider construction stays explicit. Use a unit struct or a hand-written container factory when a provider needs literal configuration, defaults, or custom initialization logic.
Use #[injectable(transient)] or #[injectable(request)] when a provider should not use the default singleton lifetime. Request-lifetime injectables generate scope-aware registration code, so Inject<T> and Optional<T> fields are resolved through the active RequestScope.
Lazy<T> defers resolution until the dependency is actually needed:
let container = Arc::new(container);
let lazy_pool = Lazy::new({
let container = Arc::clone(&container);
move || container.inject::<DatabasePool>()
});
let pool = lazy_pool.get()?;
Factory<T> creates a fresh value on every call and preserves any construction error:
let ids = Factory::new(|| Ok(RequestId::new()));
let first = ids.create()?;
let second = ids.create()?;
The default provider lifetime is expected to be singleton. Request-scoped providers are opt-in and must be resolved through an explicit request scope because they add request path overhead:
container.register_transient::<CorrelationId, _>(|_container| Ok(CorrelationId::new()))?;
container.register_request::<RequestId, _>(|_container| Ok(RequestId::new()))?;
container.register_request_scoped::<RequestState, _>(|scope| {
Ok(RequestState::new(scope.inject::<RequestId>()?))
})?;
let scope = container.request_scope();
let request_state = scope.resolve::<RequestState>()?;
let scoped_state = scope.scoped::<RequestState>()?;
Use register_request_scoped when a request-lifetime provider depends on another request-lifetime provider. The factory receives the active RequestScope, so nested request dependencies reuse the same per-request instances.
A register_request factory receives only &Container (not the request scope), so it cannot resolve other request-lifetime providers. A container.inject::<OtherRequest>() inside it returns RequestScopeRequired. Use register_request_scoped whenever a request provider needs to chain request-lifetime dependencies.
HTTP applications can attach request_scope_layer(container) to create a fresh scope for each request. Route handlers can then accept RequestScoped<T> to resolve a request-lifetime provider directly from that scope.
Resolving a request-scoped provider through the root container returns a RequestScopeRequired error instead of silently behaving like a transient provider.