Skip to content

Providers

This page continues blog from Modules — step ② onward, before the tutorial posts/ layout. Once handlers live in posts/, the container still does the same job: build each provider once (or per scope), wire #[inject] fields, fail at boot if something is unreachable.

flowchart LR
  A["PostsModule<br/>registers PostsService"] --> B["PostsHttpModule<br/>registers PostsController"]
  B --> C["PostsController<br/>#[inject] PostsService"]

A provider is any struct the container can build. Mark it #[injectable], list it in a module’s providers = [...], declare dependencies as #[inject] fields.

crates/features/src/posts/service.rs
use std::sync::Arc;
use nest_rs_core::injectable;
use sea_orm::DatabaseConnection;
#[injectable]
pub struct PostsService {
#[inject]
db: Arc<DatabaseConnection>,
}
impl PostsService {
pub async fn list(&self) -> Vec<Post> { /* ... */ }
}

#[injectable] registers PostsService by TypeId. The #[inject] db field says: resolve Arc<DatabaseConnection> from the container when you build me — typically seeded by DatabaseModule::for_root(...).

Controllers, resolvers, and gateways are providers too — they just also mount on a transport:

crates/features/src/posts/http/controller.rs
use std::sync::Arc;
use nest_rs_http::{controller, routes};
#[controller(path = "/posts")]
pub struct PostsController {
#[inject]
svc: Arc<PostsService>,
}
#[routes]
impl PostsController {
#[get("/")]
async fn list(&self) -> Vec<Post> {
self.svc.list().await
}
}
crates/features/src/posts/module.rs
#[module(providers = [PostsService])]
pub struct PostsModule;
crates/features/src/posts/http/module.rs
#[module(
imports = [PostsModule],
providers = [PostsController],
)]
pub struct PostsHttpModule;

PostsModule owns the service. PostsHttpModule owns the controller and imports the port so PostsController can inject PostsService.

flowchart TB
  PM[PostsModule] --> PS[PostsService]
  PHM[PostsHttpModule] --> PM
  PHM --> PC[PostsController]
  PC -. "#[inject]" .-> PS

Anything with a framework struct-level decorator is a provider — built by the container and listed in providers = [...]:

DecoratorRole in blog
#[injectable]PostsService — data layer
#[controller]PostsController — HTTP routes
#[interceptor]Cross-cutting HTTP wrapper (logging, DB context, …)

Other transports use the same idea (#[resolver], #[gateway], #[processor], …) when you add them. See their categories when you need them — not required to compose a minimal HTTP app.

Every provider implements Discoverable; transports mount only what the import tree reaches (Modules).

#[injectable] // singleton — default
pub struct PostsService { /* ... */ }
#[injectable(scope = request)] // one instance per request
pub struct RequestLogger { /* ... */ }
#[injectable(scope = transient)] // fresh instance every resolution
pub struct IdGenerator { /* ... */ }
ScopeBuiltShared as
singleton (default)Once at bootArc<T> everywhere
requestOnce per requestCached for that request
transientOn every resolutionNever cached

SingletonsPostsService, PostsController, the DB pool: built at boot, injected as Arc<T>.

Request-scoped — may inject singletons; singletons may not inject request-scoped types (they exist before any request). Reach them through the request boundary, not #[inject] on a singleton:

use nest_rs_http::Scoped;
#[get("/trace")]
async fn trace(&self, log: Scoped<RequestLogger>) -> String {
log.line("listed posts");
"ok".into()
}

HTTP installs a fresh request scope per call. GraphQL and MCP are singleton-only for request scope today.

Transient — rebuild on every Scoped<T> / get::<T>() extraction. Use for throw-away per-call state (a fresh correlation id, a one-shot builder). A transient must not depend on itself — cycles panic at resolution with a clear diagnostic.

The access graph — wiring checked at boot

Section titled “The access graph — wiring checked at boot”

The container is flat at runtime — every registered TypeId is globally resolvable — but #[module] makes wiring declarative. At boot the access graph checks: every #[inject] on a provider must be reachable from that provider’s module through imports, or be global infrastructure (DB pool, config, …).

Typical mistake — controller registered without importing the port:

// ✗ PostsModule not in imports — PostsService unreachable here
#[module(providers = [PostsController])]
pub struct PostsHttpModule;
Terminal window
Error: module access violation:
`PostsController` (in module `PostsHttpModule`) depends on `PostsService`, but
`PostsHttpModule` imports no module that provides it. `PostsService` is
provided by `PostsModule` — add `PostsModule` to
`#[module(imports = [...])]` of `PostsHttpModule`.

The message names the consumer, the missing type, and the module that owns it. Boot fails before HTTP listens — not on the first request.

The same check applies to layers bound with #[use_guards(...)] / #[use_filters(...)] / #[use_interceptors(...)]: if a controller lists a guard, its module must import a module that provides that guard.

flowchart TB
  PHM[PostsHttpModule]
  PM[PostsModule]
  PC[PostsController]
  PS[PostsService]
  PHM -->|imports| PM
  PHM --> PC
  PC -. needs .-> PS
  PM --> PS

Hiding the impl — pub trait + as dyn Trait

Section titled “Hiding the impl — pub trait + as dyn Trait”

When another feature must call into posts/ without naming the concrete struct, expose a pub trait and keep the impl module-private:

crates/features/src/posts/service.rs
use std::sync::Arc;
#[async_trait]
pub trait PostsService: Send + Sync {
async fn list(&self) -> Result<Vec<Post>, Error>;
}
#[injectable]
pub(crate) struct PostsServiceImpl {
#[inject]
db: Arc<DatabaseConnection>,
}
#[async_trait]
impl PostsService for PostsServiceImpl { /* ... */ }
posts/module.rs
#[module(providers = [PostsServiceImpl as dyn PostsService])]
pub struct PostsModule;

Consumers inject Arc<dyn PostsService>, never PostsServiceImpl. There is no exports = [...] list — Rust visibility plus the as dyn Trait binding is the encapsulation primitive.

let svc: Arc<PostsService> = container.get::<PostsService>().unwrap();

Container::get / get_dyn resolve imperatively and bypass the access graph. Fine for tests and rare bootstrap glue; in application code, prefer #[inject] so refactors stay checked at boot.

  • Modules — where providers are listed and imports compose.
  • Guards — providers bound at the request edge.
  • Database / Repo — the choke point every service reaches the DB through.