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.
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 reads their own org, and sees 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": "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 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 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
(2 cores, -t2 -c64 -d20s). Synthetic baseline: same handler shape and
hardware — illustrates framework overhead, not a domain-heavy API.
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:
nestrs 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.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:
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:
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.
One product, four apps — A scales on traffic spikes, B stays lean, C follows backlog, D only when needed.