Skip to content

Fundamentals

Modules, providers, guards, pipes, interceptors, filters — the vocabulary every NestRS app is built from.

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, never with a deep Cannot resolve at first request.

This category walks blog through the Publish story — from an inline root (step ①: BlogService / BlogController in apps/blog/, no database) to a composed API with a posts/ feature, the stage the tutorial builds in the repo — and the primitives every binary leans on:

  • Modules — the composition unit. #[module] declares what a module owns and what it depends on.
  • Providers — the things modules own. Anything #[injectable] (services, controllers, …) is built once and shared as Arc<T>.
  • Lifecycle — the moments an app comes alive and winds down. #[hooks] tags async methods for the init and shutdown phases the framework drains in order. See Lifecycle.
  • Guards — decide who gets through. Guard::check_http(&mut Request) returns Err(Denial) to short-circuit the chain, or attaches request-scoped context the handler reads back via Ctx<T>.
  • Pipes — convert and validate input at the request boundary. Stateless transforms (Parse<T>, ParseUuid, ValidationPipe<T>, …) bound by HTTP through the Valid<E> / Piped<P, E> extractors.
  • Interceptors — wrap handler execution. The same seam that lets you log a request also installs the ambient database transaction and the authorization context.
  • Error handling — map errors to responses. #[use_filters(...)] for route-shaped mapping; #[use_exception_filters(...)] for typed catches. See Error handling.
  • Middleware — NestRS doesn’t have a middleware primitive. See the page for the Guards/Interceptors split if you’re looking for one.

Step ① of the blog story. The domain lives in crates/features/, where every app can reach it; the app crate under apps/ only wires modules together, which is why no service.rs or controller.rs appears there.

crates/features/src/blog/service.rs
use nest_rs::core::injectable;
#[injectable]
#[derive(Default)]
pub struct BlogService;
impl BlogService {
pub fn tagline(&self) -> &'static str {
"A minimal blog API"
}
}

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

crates/features/src/blog/http/controller.rs
use std::sync::Arc;
use nest_rs::http::{controller, routes};
use crate::blog::BlogService;
#[controller(path = "/")]
pub struct BlogController {
#[inject]
svc: Arc<BlogService>,
}
#[routes]
impl BlogController {
#[get("/")]
#[public]
async fn root(&self) -> &'static str {
self.svc.tagline()
}
}

A controller is also a provider. #[inject] fields are resolved by type from the container; #[routes] mounts the verb methods on the controller’s path. #[public] is the route’s posture — every route declares one, and an unguarded route with no posture is flagged at boot (unguarded routes detected). This tagline is open on purpose, so it says so; Guards covers the other half.

apps/blog/src/module.rs
use nest_rs::core::module;
use nest_rs::http::{HttpConfig, HttpModule};
use features::blog::BlogController;
use features::blog::BlogService;
#[module(
imports = [HttpModule::for_root(HttpConfig { port: 3005, ..Default::default() })],
providers = [BlogService, BlogController],
)]
pub struct BlogModule;
apps/blog/src/main.rs
use nest_rs::core::App;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
App::builder()
.module::<BlogModule>()
.build()
.await?
.run()
.await
}
Terminal window
$ curl http://localhost:3005/
A minimal blog API

Later pages move the domain into posts/ (PostsService, PostsController, PostsModule, …) and grow BlogModule into a composed root — see Modules. The tutorial is where you implement that composed root in the repo.

  1. Collected every provider in the import tree — BlogService, BlogController — into a link-time registry.
  2. Built the access graph from BlogModule. BlogController declares it injects BlogService; BlogModule owns both; reachable. ✓
  3. Constructed each provider once. BlogService::default() produced one Arc<BlogService>, injected into BlogController::svc.
  4. Mounted BlogController on the HTTP transport from the route table #[routes] generated.

Comment the line that lists BlogService in BlogModule’s providers, then build:

Terminal window
Error: unmet dependency: `BlogController` (in module `BlogModule`) depends on `BlogService`, but no module provides it and it is not global infrastructure (a seed or factory output). Add a provider for `BlogService` to a module reachable from the root, or seed it at `App::builder()`.

The error names the consumer (BlogController), the missing dependency (BlogService), and the two ways to fix it. Boot fails inside build().await, before the HTTP transport starts — never at the first request.

  • Module — a struct decorated with #[module], listing providers it owns and imports it depends on. Modules walks blog from here to a composed root.
  • Provider — any injectable struct (#[injectable], #[controller], …). Built once, shared as Arc<T>. Providers follows the same posts/ split.
  • #[inject] field — declares “I need this type from the container.”
  • Access graph — 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.

Each primitive below gets its own page — concept, signature, where to bind it, and a tested example. Handler types (controllers, resolvers, gateways, processors, MCP tools) are transport-specific and live in their own categories: HTTP, GraphQL, WebSockets, Queue, MCP.

  • Modules — the composition unit, imports, and the port + adapters layout.
  • Providers — injectables, scopes, and the boot-time access graph.
  • Guards — pre-handler gates, the three scopes, and declaring on the provider.
  • Pipes — parse, normalize and validate at the request boundary.
  • Interceptors — wrap handler execution, install ambient state, shape the response.
  • Error handling — typed catches, unconditional mapping, and the defaults underneath.
  • Lifecycle — #[hooks] on the init and shutdown phases the framework drains in order.
  • The request lifecycle — one pool per layer family, deduplicated by TypeId, run exactly once.
  • Middleware — why there is no middleware primitive, and which of the two roles you actually want.
  • Correlation — the W3C trace every unit of work runs under, readable anywhere.
  • Extending NestRS — the extension contract and its three selection modes.
  • Architecture and naming — the role tables, the five naming levels, and the reserved vocabulary.
  • Tutorial — build the composed posts/ root end to end.
  • HTTP — the first transport to mount on this vocabulary.