Skip to content

Why NestRS

Scalable Rust backend apps with native performance.

Cross-cutting concerns — security, transactions, input conversion, lifecycle, discovery — are error-prone exactly because they are repetitive. Every place a codebase has to wire them by hand is a place they will eventually be forgotten, inconsistent, or wrong. NestRS treats this as a framework problem: those concerns must be transparent to the application code.

The leverage is procedural macros. A controller, a resolver, a processor is a struct with a decorator; the framework expands it into the boilerplate a contributor would otherwise have written by hand — and would, eventually, have written slightly differently in two different files.

Two shifts make this trade-off worth reopening.

Memory is now a line item. Cloud RAM has grown steadily more expensive, and for many services it is the largest single part of the bill. Managed runtimes — Node among them — are genuinely productive, but they buy that productivity with a garbage collector and a runtime whose footprint is always resident, whether or not a request is in flight. You pay for it idle, and you pay for it again in energy per request.

Native code is no longer the hard path. Much of the boilerplate that once made higher-level runtimes feel faster to ship — scaffolding, ceremony, repetitive wiring — is easier to absorb today. The friction that justified the runtime tax has largely dissolved.

NestRS reopens the trade-off between the two: keep the declarative, decorator-driven style that makes the managed-runtime model productive, but stand it on a native, compiled foundation that doesn’t bill you for it in RAM. One cargo step compiles and type-checks, modules wire up regardless of import order, and the result ships as one lean binary.

Six consequences of the thesis — not a feature list. They compose: drop any one and the others weaken. Each section below states the guarantee; the mechanics live in Fundamentals, Security, and Database.

Modules, providers, controllers, resolvers, gateways, processors — each is a struct decorated with an attribute macro. The decorator carries the entire integration contract. There is no service locator to call, no registration list to keep in sync, no central manifest a contributor has to remember to edit.

#[controller(path = "/users")]
pub struct UsersController {
#[inject]
svc: Arc<UsersService>,
}

Once the security modules are imported, every read through the data layer is filtered, every mutating write is gated, and every response body is masked. A feature does not opt in to authn and authz; it opts out by not importing them.

Security is structural, not vigilant: forgetting a check is a category error the framework prevents, not a bug a code reviewer has to catch.

A mutating HTTP request installs a transactional executor before the handler runs. The service reaches the database only through Repo, which picks up that executor automatically — commit on 2xx/3xx, rollback otherwise. Worker contexts install a pool executor for the same code path.

// Ambient executor — no per-method ceremony.
impl UsersService {
pub async fn rename(&self, id: Uuid, name: String) -> Result<User, ServiceError> {
let user = Repo::<Users>::find_by_id(id)
.await?
.ok_or_else(|| ServiceError::not_found("user"))?;
let mut active = user.into_active_model();
active.name = Set(name);
Ok(User::from(&Repo::<Users>::update(active).await?))
}
}

A feature is a port (entity, service, contract) plus one adapter per transport it exposes. The port sits at the feature root; each adapter lives in its own sub-folder with its own module.rs. An app picks the edges it serves — a worker imports the queue adapter; an API imports HTTP, GraphQL, and WS.

  • Directoryfeatures/users/
    • module.rs, service.rs, entity.rs, dto.rs, error.rs
    • Directoryhttp/
    • Directorygraphql/
    • Directoryws/
    • Directoryqueue/
    • Directorymcp/

See Modules — Port + adapters for the composition rule and the reference layout.

The dependency-injection graph is not resolved by reflection. Every module records its imports and its providers’ dependencies at compile time; at startup, App::build() walks the graph and fails with a clear error if a provider injects something its module cannot reach.

A misconfigured import is a startup error naming the missing dependency, not a Cannot resolve at first request. See Providers — The access graph.

Capabilities ship as separate crates and integrate via discovery gated on module reachability. A binary that imports only the queue adapter does not mount HTTP routes or GraphQL resolvers — even when those files live in the same shared feature crate. One codebase, multiple deployable shapes.

On the introduction page, the same Hello World service benchmarks at roughly 25× the throughput and ~20× less RAM than an equivalent Node stack on identical hardware.

  • Not an HTTP layer. NestRS sits on top of hyper / tokio / poem — it gives them structure, it does not replace them.
  • Not a thin convenience layer. The framework’s value is in the cross-cutting guarantees (authz, transactions, masking, boot-time wiring). A controller-only decorator on top of an existing stack would not deliver them.
  • Not a runtime DI container. The container exists, but the contract it enforces is static: types and module imports, checked at boot. There is no resolve<T>() you are expected to call from user code.
  • Getting started — install, run an app, build your first feature.
  • The Publish workspace — the fictional product every example app tells.
  • Tutorial — a complete feature, end to end, with every layer wired.
  • Fundamentals — modules, providers, guards, pipes, interceptors, filters.