Why NestRS
The thesis behind the framework, who it is for, and the six structural properties that follow from it.
The thesis
Section titled “The thesis”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.
Who it is for
Section titled “Who it is for”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.
Why now
Section titled “Why now”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.
How it delivers
Section titled “How it delivers”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.
1. Declarative wiring
Section titled “1. Declarative wiring”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>,}2. Secure by default
Section titled “2. Secure by default”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.
3. Transactional by default
Section titled “3. Transactional by default”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.
// 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?)) }}4. Multi-transport, one shape
Section titled “4. Multi-transport, one shape”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.
5. Wiring verified at boot
Section titled “5. Wiring verified at boot”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.
6. Lean per-binary
Section titled “6. Lean per-binary”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.
What this is not
Section titled “What this is not”- 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.
Going further
Section titled “Going further”- 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.