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.
The feature
Section titled “The feature”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>.
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.
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.
use nestrs_core::module;use super::{controller::HelloController, service::HelloService};
#[module(providers = [HelloService, HelloController])]pub struct HelloModule;Composing into an app
Section titled “Composing into an app”The root module of a binary imports every feature module the binary exposes.
use nestrs_core::module;use crate::hello::HelloModule;
#[module(imports = [HelloModule])]pub struct AppModule;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}Run it
Section titled “Run it”just dev app 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$ curl http://localhost:3000Hello WorldWhat the framework did
Section titled “What the framework did”- Collected every provider in the import tree —
HelloService,HelloController— into a static registry at link time. - Built the access graph from
AppModule.HelloControllerdeclares it injectsHelloService;HelloModuleownsHelloService; the graph is reachable. ✓ - Constructed each provider once.
HelloService::default()produced oneArc<HelloService>.HelloControllerwas built with thatArcinjected into itssvcfield. - Mounted
HelloControlleron the HTTP transport using the route table that#[routes]generated.
Try breaking it
Section titled “Try breaking it”Comment the line that lists HelloService in the module’s providers, then
cargo run:
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.
Five ideas to remember
Section titled “Five ideas to remember”- Module — a struct decorated with
#[module], listingprovidersit owns andimportsit depends on. - Provider — any injectable struct (
#[injectable],#[controller],#[resolver],#[gateway],#[processor], …). Built once, shared asArc<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.
Going further
Section titled “Going further”Real-world apps stack many modules. The api example imports a dozen at
once — feature modules plus framework modules:
#[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.