Skip to content

Why NestRS

The thesis behind the framework, who it is for, and the six structural properties that follow from it.

The Rust framework for modular, scalable backends — you write the business logic, it carries the rest.

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.

Three starting points, one framework. What differs is which half of the thesis above you came for.

  • You write NestJS or Spring, and you want Rust’s memory and latency profile. The layer vocabulary carries over intact; what changes is that the guarantees are checked rather than remembered. Coming from NestJS maps every reflex.
  • You write Rust, and a router is below the altitude of your application. axum and tower do their job; tenancy, policy, transactions and masking are still yours to keep correct across every query. Why not axum compares the two on that axis.
  • You are starting a backend that has to hold its shape as the team grows. Modules, an access graph checked at boot, and one place per concern — the structure is the framework’s, not a convention your reviews defend.

A service with no cross-cutting concerns — a proxy, a single-tenant tool, an embedded component — is better served by a bare router, and that answer is part of the thesis rather than an exception to it.

Two shifts make this trade-off worth reopening.

RAM decides your instance class. A Node service idling at 100 MB fixes the smallest box you can rent before it serves a request; a 5 MB binary packs an order of magnitude more services per node. 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.

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 carried by the framework’s decorators today.

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.

crates/features/src/users/http/controller.rs
#[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 per-attempt transaction on the same code path.

crates/features/src/users/service.rs
// 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 — Extract a feature 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 benchmarks page, the same service — idiomatic on both sides, byte-identical contract — measures at ×2.5 the throughput of NestJS on Fastify (its best case), ×4.2 on Express, in ~25× less RAM, cold-starting ~23× faster.

Six guarantees are worth what checks them. 2,000+ tests cover the framework workspace, and the end-to-end suites run against live Postgres, Redis and S3 rather than mocks — wiring bugs are the ones unit tests never see. The 120+ pages on this site are linted against the framework’s own source, so a page that contradicts the code fails the build instead of shipping.

  • 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 is drawn from.
  • Tutorial — a complete feature, end to end, with every layer wired.
  • Fundamentals — modules, providers, guards, pipes, interceptors, filters.