Skip to content
NestRS logo

NestRS

Multi-tenant isolation you cannot forget. Row-level filtering, per-field masking, and transactions — transparent, non-optional, verified at boot. You write a controller, a service, a module; decorators carry the security and the wiring. Native speed comes free: ~25× Node's hello-world throughput in 4–6 MB of RAM — the full auth + row-level + masking pipeline, Postgres included, holds ~23k req/s at p99 < 4.5 ms in ~32 MB. Both measured below. Familiar if you come from NestJS, Spring, or Laravel. HTTPOpenAPIGraphQLWebSocketsMCP DatabaseQueueScheduleOpenTelemetry AuthenticationOAuth2Authorization

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.

crates/features/src/orgs/http/controller.rs (abridged)
#[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:

crates/features/src/authz/ability.rs (abridged)
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.

See it live — the security checkpoint →

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
hello/src/controller.rs
Write
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()
}
}
 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.

How these numbers were taken

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:

Terminal window
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.

NestRS NestJS 11 · Node 24
Throughput 2 cores · req/s
NestRS ~463k
Node ~18k
~25×
Throughput per core · req/s
NestRS ~231k
Node ~17k
~13×
Latency p50 lower is better
NestRS 0.13 ms
Node 3.2 ms
~24×
Latency p99 lower is better
NestRS 0.57 ms
Node 6.4 ms
~11×
Memory idle lower is better
NestRS 4 MB
Node 80 MB
~20×
Memory under load lower is better
NestRS 6 MB
Node 118 MB
~18×

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:

Publicexposed to clients and partners
  • App A:3000a public surface
  • App B:3001another public surface
shared featuresone model — every app above and below builds on it
Privatenon-public · off the client request path
  • App Cbackground work
  • App Dmore background work

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.

App A:3000demand · high traffic×5
App B:3001demand · steady×2
App Cqueue · backlog×4
App Dqueue · light×2

One product, four apps — A scales on traffic spikes, B stays lean, C follows backlog, D only when needed.

  • Getting started — scaffold and run your first app.
  • Tutorial — build a posts feature end to end.
  • Why NestRS — the thesis and the structural properties behind it.
  • Threat model — what each layer catches, what it doesn’t, and where the framework defers to your deployment.
  • The Publish workspace — the product universe every example is drawn from.