Secure & transactional by composition
Authn, authorization, row-level filtering, response masking, transaction scope — turned on by importing a module, never by remembering to call them.
Authenticated, tenant-scoped, transactional, field-masked — and the impl
block is empty on purpose. The two guards declare the posture, #[crud]
generates the five routes.
#[controller(path = "/orgs")]#[use_guards(AuthnGuard, AuthzGuard)]pub struct OrgsController { #[inject] svc: Arc<OrgsService>,}
#[crud(service = svc, entity = OrgEntity, output = Org, create = CreateOrg, update = UpdateOrg)]impl OrgsController {}One ability decides the rows and the fields, in one place:
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 does the rest:
❯ 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 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. Behind the guards sits the full stack — JWT on
EdDSA keys, OAuth2 with PKCE, Argon2id passwords, social login — each a module
import (Security).
No wiring file, no config, no boilerplate. The hard parts of framework Rust — lifetimes in middleware, trait gymnastics, generic bounds — stay inside the framework; your files hold structs, decorators and async fns.
❯ nestrs new hello --standalone
Created standalone nestrs app at ./hello
+ hello/Cargo.toml
+ hello/rust-toolchain.toml
+ hello/.gitignore
..
+ hello/src/service.rs
+ hello/src/controller.rs
+ hello/tests/integration/main.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("/")] #[public] 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 crate::controller::HelloController;use crate::service::HelloService;
#[module( imports = [ HttpModule::for_root(None), ], providers = [HelloService, HelloController],)]pub struct HelloModule;use anyhow::Result;use nest_rs_config::Environment;use nest_rs_core::App;
use hello::HelloModule;
#[tokio::main]async fn main() -> Result<()> { let _environment = Environment::init();
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 As with any Rust dependency, the first nestrs run dev compiles the crate graph once. Every run after is the sub-second incremental rebuild shown above.
Scaffold a standalone crate, grow into a workspace when you add apps — the same inject-and-decorate model carries every surface: HTTP, GraphQL, WebSockets, queues, scheduled jobs, MCP. Get started → · Why NestRS →
The same service written twice, each side the way its own CLI scaffolds it, on both Fastify and Express. Both serve a byte-identical HTTP contract at the same stack depth, and a conformance gate verifies the bytes before anything is measured. One pinned core each — the regime that favours Node.
The harness lives in bench/
— provider manifests, the byte-for-byte conformance gate, and the runner.
Server and load generator (oha) are pinned to
disjoint CPU sets; each tier gets a discarded warmup, then repeated timed runs
whose medians are reported; every result embeds its machine fingerprint (CPU,
kernel, toolchains, resolved dependency versions). Node runs NODE_ENV=production
on Node 24; NestRS is a release build.
Per-core is the honest comparison: in production Node scales out by clustering — one ~200 MB process per core — while one NestRS binary takes every core in a single single-digit-MB process. The published run comes from a 4-core / 8 GB Docker VM (Apple Silicon) — virtualized, so these figures are a floor rather than a ceiling. One command reproduces the table on your hardware:
cd bench && just benchThe benchmark page documents the contract, the full protocol and its limits.
Secure & transactional by composition
Authn, authorization, row-level filtering, response masking, transaction scope — turned on by importing a module, never by remembering to call them.
Wiring you find out about at boot, not at 3am
The DI graph is checked at boot: a bad import fails startup with the missing wire named. No reflection, no resolution error five minutes after deploy.
Declarative, decorator-driven
#[module], #[controller], #[resolver], #[processor], #[gateway],
#[scheduled], #[mcp] — expanding to plain Rust you can read with
cargo expand.
Types you don't fight
Rust types end to end — entity, DTO, handler, dataloader, GraphQL schema,
OpenAPI doc. No any, no unknown, no casts at the boundaries.
Native throughput
×2.5 NestJS-on-Fastify — its best case — and ×4.2 Express, with sub-millisecond p99 and no GC pauses (benchmarks).
An order of magnitude less RAM
~8 MB under load where the same NestJS service holds ~200 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, queues, scheduling, events, MCP, authorization, health probes, OpenTelemetry — each an opt-in crate, so a worker compiles no HTTP stack at all (Packages).
Every capability above ships today, each with a reference section on this site.
Microservice scaling without the distributed-systems tax. 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. Self-contained tokens and a
shared database — no chatty RPC, 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 — schematically:
Same domain, shared features — one monorepo or several; which app runs where is an operational choice, not a rewrite of business logic.
Each app scales horizontally on its own signal — a hot public surface follows demand, background apps follow queue depth. Same binary, more replicas, per app; 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.
Built by YV17labs