Skip to content

Core concepts

A NestRS app is a tree of modules. Each module groups providers (injectable structs) and optionally imports other modules. The dependency-injection graph is built at compile time and verified at boot — a missing wire fails startup with a clear error.

This page walks the app example end to end. It is the smallest possible NestRS service — three files, one route.

  • Directoryapps/app/src/
    • Directoryhello/
      • service.rs
      • controller.rs
      • module.rs
    • app.rs
    • main.rs

A service is an injectable struct. Everything decorated with #[injectable] is registered with the container and shared as Arc<T>.

apps/app/src/hello/service.rs
use nestrs_core::injectable;
#[injectable]
#[derive(Default)]
pub struct HelloService;
impl HelloService {
pub fn greeting(&self) -> String {
"Hello World".to_string()
}
}

A controller is also injectable. #[inject] fields are resolved by type from the container. #[routes] mounts the verb methods on the controller’s path.

apps/app/src/hello/controller.rs
use std::sync::Arc;
use nestrs_http::{controller, routes};
use super::service::HelloService;
#[controller(path = "/")]
pub struct HelloController {
#[inject]
svc: Arc<HelloService>,
}
#[routes]
impl HelloController {
#[get("/")]
async fn hello(&self) -> String {
self.svc.greeting()
}
}

A module lists the providers it owns. Other modules import this one to gain access to those providers.

apps/app/src/hello/module.rs
use nestrs_core::module;
use super::{controller::HelloController, service::HelloService};
#[module(providers = [HelloService, HelloController])]
pub struct HelloModule;

The root module of a binary imports every feature module the binary exposes.

apps/app/src/app.rs
use nestrs_core::module;
use crate::hello::HelloModule;
#[module(imports = [HelloModule])]
pub struct AppModule;
apps/app/src/main.rs
use nestrs_core::App;
use nestrs_http::HttpTransport;
use app::AppModule;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
App::new::<AppModule>()?
.transport(HttpTransport::new().bind("0.0.0.0:3000"))
.run()
.await
}
Terminal window
just dev app
Terminal window
Compiling app v0.1.0
Finished `dev` profile in 4.21s
Running `target/debug/app`
2026-06-03T10:14:22.183Z INFO nestrs::http: bound 1 route on 0.0.0.0:3000
GET / → HelloController::hello
Terminal window
$ curl http://localhost:3000
Hello World
  1. Collected every provider in the import tree — HelloService, HelloController — into a static registry at link time.
  2. Built the access graph from AppModule. HelloController declares it injects HelloService; HelloModule owns HelloService; the graph is reachable. ✓
  3. Constructed each provider once. HelloService::default() produced one Arc<HelloService>. HelloController was built with that Arc injected into its svc field.
  4. Mounted HelloController on the HTTP transport using the route table that #[routes] generated.

Comment the line that lists HelloService in the module’s providers, then cargo run:

Terminal window
Error: cannot register provider(s) — unreachable in graph:
- HelloController (needs HelloService)

The error names what is missing and why it cannot be resolved. The build does not start the HTTP transport when wiring is invalid.

  • Module — a struct decorated with #[module], listing providers it owns and imports it depends on.
  • Provider — any injectable struct (#[injectable], #[controller], #[resolver], #[gateway], #[processor], …). Built once, shared as Arc<T>.
  • #[inject] field — declares “I need this type from the container.”
  • Access graph — the boot-time check that proves every #[inject] resolves to a provider the module can reach.
  • Discoverable — every decorated provider registers itself; transports consume the registry filtered by reachability.

Real-world apps stack many modules. The api example imports a dozen at once — feature modules plus framework modules:

apps/api/src/app.rs
#[module(
imports = [
ConfigModule::for_root(),
DatabaseModule::for_root(None),
AuthnCoreModule,
AuthzHttpModule,
AuthzGraphqlModule,
UsersHttpModule,
UsersGraphqlModule,
GraphqlModule::for_root(None),
OpenApiModule::for_root(None),
TelemetryModule,
ServerTimingModule,
],
)]
pub struct AppModule;

Each import contributes its providers; each transport (HttpTransport, Scheduler, QueueWorker, …) drains its inventory registry filtered by what the import tree reaches. Linking a crate without importing its module keeps its providers inert — built but not mounted.

  • HTTP — controllers, routes, guards, pipes, filters, interceptors.
  • Database — services, Repo, transactions, the ambient executor.
  • Security — authn strategies, ability-based authorization.