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. For teams shipping structured Rust backends, especially if NestJS shaped how you think about modules: you write a controller, a service, a module; the decorators carry security and wiring. And it stays lean — the full auth + authz + row-level + masking pipeline serves ~23k req/s at p99 < 4.5 ms in ~32 MB of RAM, Postgres included. HTTPOpenAPIGraphQLWebSocketsMCP QueueScheduleOpenTelemetry 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 reads their own org, and sees 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": "admin@acme.test", … }, …]

 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

See it live — the security checkpoint →

No wiring file. No config. No boilerplate. 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 (2 cores, -t2 -c64 -d20s). Synthetic baseline: same handler shape and hardware — illustrates framework overhead, not a domain-heavy API.

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

Declarative, decorator-driven

#[module], #[controller], #[resolver], #[processor], #[gateway], #[scheduled], #[mcp]. You write logic; the decorator expands the boilerplate. 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. 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 — versus ~80–120 MB for an equivalent Node service. 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.

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.

Verified before it serves

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.

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.

Microservice scaling without the distributed-systems tax. Each app ships as one static native binary sharing a single domain model — you split how it deploys and scales, not the code it’s built from.

A workload is one deployable binary with one operational job — HTTP API, WebSocket gateway, queue worker, MCP server, and so on. NestRS supports several workloads for the same product in either layout:

  • One monoreponestrs new scaffolds a workspace: shared logic in crates/features/, thin composition roots under apps/*. Add another workload with nestrs new <name> from the same tree.
  • Several repositories — each workload can live in its own crate or repo; domain code stays in a shared features library both sides depend on. No chatty RPC required — self-contained tokens and a shared database are enough.

The standalone demo above is one workload in one crate. As the product grows, splitting by workload is how you keep that model without rewriting business logic:

  • Lean binaries — import only the transports an app serves; a worker never pulls in HTTP or GraphQL it will not run.
  • Independent scaling — scale the request path on traffic, background work on queue depth; no single “scale everything” knob.
  • Operational boundaries — who runs where is a composition choice in each app’s module.rs, not a fork of entities and services.

Entities, services, and policy are written once in features. Each workload has a clear responsibility — some are public (surfaces clients and partners call), others stay private — non-public workloads, off the request path. Your workloads stay loosely coupled: shared contracts and data, not a platform team or service mesh to get started.

See Getting started and the CLI to scaffold a workspace. In that layout, each workload is an app under apps/<name>/ — 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.

Splitting by responsibility is only the first step. In production you scale horizontally per app — the same binary, more replicas when its signal moves. A hot public surface follows demand; a steady one keeps a small footprint; background apps follow queue depth. There is no single “scale the monolith” knob — each app under apps/* gets its own replica count.

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.