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 in one glance
Section titled “A provider in one glance”A provider is any struct the container can build. Mark it
#[injectable], list it in a module’s providers = [...], declare
dependencies as #[inject] fields.
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:
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 }}#[module(providers = [PostsService])]pub struct PostsModule;#[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
What counts as a provider
Section titled “What counts as a provider”Anything with a framework struct-level decorator is a provider — built by
the container and listed in providers = [...]:
| Decorator | Role 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).
Scopes — singleton, request, transient
Section titled “Scopes — singleton, request, transient”#[injectable] // singleton — defaultpub struct PostsService { /* ... */ }
#[injectable(scope = request)] // one instance per requestpub struct RequestLogger { /* ... */ }
#[injectable(scope = transient)] // fresh instance every resolutionpub struct IdGenerator { /* ... */ }| Scope | Built | Shared as |
|---|---|---|
singleton (default) | Once at boot | Arc<T> everywhere |
request | Once per request | Cached for that request |
transient | On every resolution | Never cached |
Singletons — PostsService, 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;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:
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 { /* ... */ }#[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.
The runtime escape hatch
Section titled “The runtime escape hatch”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.
Going further
Section titled “Going further”- 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.