Secure & transactional by composition
Authn, authorization, row-level filtering, response masking, transaction scope — you turn them on by importing a module, never by remembering to call them. Forgetting to redact a field becomes structurally hard.
This CRUD resource is authenticated, tenant-scoped, transactional, and
field-masked. The impl block is empty on purpose: the two guards declare the
posture, #[crud] generates the five routes, and one ability does the rest.
#[controller(path = "/orgs")]#[use_guards(AuthGuard, AuthzGuard)]pub struct OrgsController { #[inject] svc: Arc<OrgsService>,}
#[crud(service = svc, entity = OrgEntity, output = Org, create = CreateOrg, update = UpdateOrg)]impl OrgsController {}The policy lives in one place. A member sees only their own org’s users, and only the fields the ability grants:
ab.can(Action::Read, user::Entity) .when(|p| p.eq(user::Column::OrgId, actor.org_id)) .fields([user::Column::Id, user::Column::Name]);Same route, same handler — the caller’s token decides the rows and the fields:
❯ curl :3002/users -H "authorization: Bearer $ADMIN" # admin
[{ "id": "…ac00", "name": "Acme Admin", "email": "[email protected]", … }, …]
❯ curl :3002/users -H "authorization: Bearer $MEMBER" # plain member
[{ "id": "…ac00", "name": "Acme Admin" }, …] # email masked, org-scoped
❯ curl -o /dev/null -w "%{http_code}" :3002/users/<globex-id> -H "…$MEMBER"
403 # cross-tenant read refused, decided by the framework Posture is mandatory, not a convention: a GraphQL operation with neither
#[authorize] nor #[public] does not compile, and an HTTP controller whose
guard wiring is missing fails at boot — an ungated, unmasked surface cannot
ship.
Behind the guards sits a full authentication stack — JWT on EdDSA keys, OAuth2 with PKCE, Argon2id passwords with timing-safe verification, social login — each a module import, documented in Security.
No wiring file. No config. No boilerplate. And if Rust’s learning curve is
what holds you back: the hard parts of framework Rust — lifetimes in
middleware, trait gymnastics, generic bounds — live inside the framework.
What’s left in your files is structs, decorators, and async fns. Scaffold a standalone crate with
the CLI, then follow the tabs from Controller (your HTTP surface) through
Service, Module, and Main — that’s a real, type-checked service.
The dependency graph is built and verified at boot, and the crate compiles
to a native binary. Start here; grow into a workspace when you add more apps
— same decorators, shared crates/features/.
❯ nestrs new hello --standalone
Created standalone nestrs app at ./hello
Template: hello — Hello World on GET /
+ hello/Cargo.toml
+ hello/rust-toolchain.toml
+ hello/.gitignore
..
+ hello/src/controller.rs
+ hello/tests/e2e/main.rs
Mode: standalone (one crate, logic in src/)
Next steps:
cd ./hello
nestrs run dev
Open http://localhost:3000/ in your browser use std::sync::Arc;use nest_rs_http::{controller, routes};
use crate::service::HelloService;
#[controller(path = "/")]pub struct HelloController { #[inject] svc: Arc<HelloService>,}
#[routes]impl HelloController { #[get("/")] async fn hello(&self) -> String { self.svc.greeting() }}use nest_rs_core::injectable;
#[injectable]#[derive(Default)]pub struct HelloService;
impl HelloService { pub fn greeting(&self) -> String { "Hello World".to_string() }}use nest_rs_core::module;use nest_rs_http::HttpModule;use nest_rs_opentelemetry::OpenTelemetryModule;
use crate::controller::HelloController;use crate::service::HelloService;
#[module( imports = [ OpenTelemetryModule, HttpModule::for_root(None), ], providers = [HelloService, HelloController],)]pub struct HelloModule;use anyhow::Result;use nest_rs_config::Environment;use nest_rs_core::App;use nest_rs_opentelemetry::OpenTelemetry;
use hello::HelloModule;
#[tokio::main]async fn main() -> Result<()> { let _environment = Environment::init(); let _telemetry = OpenTelemetry::init("hello")?;
App::builder() .module::<HelloModule>() .build() .await? .run() .await}❯ nestrs run dev
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.17s
Running `target/debug/hello`
2026-04-29T12:00:00.799422Z INFO nest_rs::module: module dependencies initialized module="HelloModule"
2026-04-29T12:00:00.799862Z INFO nest_rs::routes: GET / (hello)
..
2026-04-29T12:00:00.802100Z INFO poem::server: listening addr=socket://0.0.0.0:3000
2026-04-29T12:00:00.802552Z INFO poem::server: server started
2026-04-29T12:00:00.803011Z INFO nest_rs::access: method=GET path=/ status=200 bytes=11 duration_ms=0.189 client_ip=127.0.0.1 user_agent=curl/8.7.1 trace_id=48643283b6e9c222dadc23480117d76a
❯ curl http://localhost:3000
Hello World The first nestrs run dev compiles the framework from source — a few minutes. Every run after is the sub-second incremental rebuild shown above.
The same inject-and-decorate model carries every surface — HTTP, GraphQL, WebSockets, queues, scheduled jobs, MCP. Get started → · Why NestRS →
The same “Hello World” service — a provider, a controller, a module — built
once in NestRS and once in NestJS 11 on Node.js 24, under identical wrk load
(each server pinned to 2 cores, -t2 -c64 -d20s). Synthetic baseline: same
handler shape and hardware — it isolates framework overhead, not a
domain-heavy API.
Two different measurements, don’t conflate them: the ~23k req/s in ~32 MB in the hero is the demo API’s full pipeline — JWT verification, ability build, a row-level-filtered Postgres query, response masking. The ~463k req/s and 4–6 MB below are the bare hello-world — no database, no auth. The gap between the two is the cost of real work (crypto + Postgres), not of the framework.
Both ran on the same machine: a Linux Docker container capped at 4 cores and 8 GB of RAM, with the load generator competing for those same cores. That setup understates the framework — on dedicated hardware, with the client off-box, every figure on this page goes up. Measure it on yours.
If your point of comparison is axum or Actix rather than Node: same native core, so the question is not speed — it is what the framework carries for you. See Why not axum?.
Read it the other way around: the fully-secured API still lands in the range Node reaches serving a bare hello-world.
Both services ran on the same machine, inside a Linux Docker container capped at 4 cores and 8 GB of RAM, each server pinned to 2 cores, with the load generator sharing the remaining cores:
wrk -t2 -c64 -d20s http://localhost:3000/The NestRS side is the scaffolded hello (nestrs new hello --standalone,
built in release); the Node side is the equivalent NestJS 11 hello-world on
Node.js 24 in production mode. The full-pipeline figures come from the same
container driving the seeded Publish api app’s GET /users with a member
JWT — verification, ability build, row-level-filtered Postgres query, and
response masking on every request. The client competing for the same cores
understates both sides — measure on your own hardware.
Secure & transactional by composition
Authn, authorization, row-level filtering, response masking, transaction scope — you turn them on by importing a module, never by remembering to call them. Forgetting to redact a field becomes structurally hard.
Wiring you find out about at boot, not at 3am
The DI graph is checked at boot — a bad import fails startup with a
clear error naming the missing wire. No reflection, no runtime
“cannot resolve dependency” five minutes after deploy. The check lives
in access.rs.
Declarative, decorator-driven
#[module], #[controller], #[resolver], #[processor],
#[gateway], #[scheduled], #[mcp]. You write logic; the decorator
expands the boilerplate — into plain Rust you can read with
cargo expand. No service locator,
no registration list to keep in sync.
Types you don't fight
Rust types end to end — entity, DTO, route handler, dataloader, GraphQL
schema, OpenAPI doc. No any, no unknown, no as casts at the
boundaries, no runtime “undefined is not a function.”
Native throughput
~463k req/s on 2 cores, ~231k per core, sub-millisecond p99,
no GC pauses (hello-world baseline — conditions above). The same
hyper/tokio core as the fastest Rust web frameworks — with a
structure on top.
An order of magnitude less RAM
~4 MB idle, ~6 MB under load for the hello-world baseline — versus ~80–120 MB for an equivalent Node service; the full demo pipeline, Postgres included, stays around ~32 MB. Smaller instances, higher density, a materially lighter cloud bill.
Boots in milliseconds
Each deployable is a static native binary with no runtime to warm up — autoscaling and cold starts stop hurting.
Batteries included, opt-in by crate
HTTP, GraphQL, OpenAPI, WebSockets, Redis-backed queues, scheduling, an event bus, MCP, CASL-style authorization, health probes, OpenTelemetry — each an opt-in crate, so a worker compiles no HTTP stack at all. See the full list on Packages.
Every capability above has a reference section on this site; nothing on this list is roadmap.
Microservice scaling without the distributed-systems tax. A workload
is one deployable binary with one operational job — HTTP API, WebSocket
gateway, queue worker, MCP server. Entities, services, and policy are
written once in a shared features crate; each app under apps/* is a
thin composition root importing only the transports it serves, so a worker
never compiles the HTTP stack. One monorepo or several repositories —
coupling stays loose either way: self-contained tokens and a shared
database, no chatty RPC and no service mesh to get started.
Who runs where is a composition choice in each app’s module.rs, not a
fork of entities and services. See Getting started
and the CLI to scaffold the workspace layout — schematically:
Same domain, shared features — one monorepo or several; which app runs where is an operational choice, not a rewrite of business logic.
In production you scale horizontally per app — the same binary, more
replicas when its signal moves: a hot public surface follows demand,
background apps follow queue depth. Each app under apps/* gets its
own replica count — no single “scale the monolith” knob.
One product, four apps — A scales on traffic spikes, B stays lean, C follows backlog, D only when needed.