Why not axum?
axum routes HTTP; NestRS adds the multi-tenant isolation you cannot forget.
You are a Rust engineer, so you already know axum. It is a focused, well-built routing and HTTP library on top of hyper and tokio, and it does that job cleanly. So the fair question is not “which is faster” — both compile to native code on the same core. The question is what each one hands you.
axum hands you a router. NestRS hands you an application framework: the cross-cutting concerns — authn, authz, row-level tenant filtering, per-field masking, transactions — are carried by the framework, checked at boot, applied by the data layer. The one thing you cannot forget is a tenant filter, because you never write it.
Here is a full authenticated, tenant-scoped, transactional, field-masked CRUD
resource for orgs. The impl block is empty on purpose.
#[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 {}The two guards declare posture; #[crud] generates the five REST operations
and re-emits them under #[routes]. Every read goes through the data layer,
which filters rows by the caller’s ambient ability. Every write runs inside a
transaction that commits on 2xx/3xx and rolls back otherwise. Every
response body is masked field by field. None of that appears in the file,
because none of it is yours to maintain. And the posture itself is not
optional: a GraphQL operation with neither #[authorize] nor #[public]
does not compile, and an HTTP controller whose guard wiring is missing
fails at boot.
What you assemble by hand with axum
Section titled “What you assemble by hand with axum”The same resource on axum is not hard to write. It is hard to keep correct across a codebase, because each concern lives in a different place and each query is a place a tenant filter can go missing.
- Routing — five handlers registered on a
Router, one per operation. - Auth — a tower middleware that validates the token and attaches a principal, plus an authorization check inside each handler.
- Tenancy — a
WHERE org_id = $1on every read, and a matching check on every write. This is the load-bearing line. Nothing in the type system reminds you when a new query forgets it. - Transactions — begin, commit, and rollback wired per mutating handler, or a middleware that installs a transaction and a discipline that every query picks it up.
- Masking — a response shape per role, applied by hand before serializing.
Each of these is one correct line and one forgotten line away from a cross-tenant leak. In NestRS the filter is applied by the data layer from the ambient ability, so forgetting it is a category error the framework prevents — not a review comment someone has to catch.
That inversion — a feature opting out of authz by not importing it, rather than into it per handler — is property 2 of the thesis, and Security is where the three layers it names are wired.
The macro question
Section titled “The macro question”“An empty impl block that turns into five routes” is where a Rust engineer
reasonably slows down. Attribute macros have a reputation: hidden control
flow, opaque errors, code you cannot read. Two properties keep NestRS’s
decorators inspectable.
The expansion is code you can read. cargo expand prints what a
decorator emitted — plain Rust, compiled by rustc like anything you write by
hand, with doc comments naming the attribute argument each item came from.
Nothing is resolved by reflection and nothing is generated at runtime.
$ cargo expand -p features orgs::http::controllerpub struct OrgsController { svc: Arc<OrgsService>,}impl OrgsController { /// The controller's route prefix, from `#[controller(path = "…")]`. pub const PATH: &'static str = "/orgs"; /// Construct this provider by resolving its `#[inject]` fields from the /// container. Emitted by the decorator; called by the register phase, /// not by hand. pub fn from_container(container: &::nest_rs::core::Container) -> Self {…Mistakes fail at compile time, pointing at your line. A mistyped option is not a cryptic trait error deep in the expansion — the first error names the option and the accepted set, at the line and column you typed it:
$ cargo check -p featureserror: unknown #[crud] argument `servcie`; expected `service`, `entity`, `output`, `create`, `update`, `ops` or `paginate` --> crates/features/src/orgs/http/controller.rs:17:5 |17 | servcie = svc, | ^^^^^^^What a decorator cannot check at compile time — that every injected dependency is reachable through your module imports — fails at boot instead, as the next section shows.
Wiring you find out about at boot
Section titled “Wiring you find out about at boot”axum resolves its state and its routes as you build the Router. A dependency
you forgot to thread through surfaces where you use it, at runtime, on the
request that needs it.
NestRS records imports and dependencies at compile time and walks that graph in
App::build(), so the same mistake is a startup error naming the consumer, the
missing dependency and the module that provides it. The check and its error are
on Providers.
Performance is the secondary benefit
Section titled “Performance is the secondary benefit”NestRS sits on top of hyper, tokio and poem — the same native core the fastest Rust frameworks use. So throughput is not the axis that separates the two: both are compiled and both are fast, and against a managed runtime the margin is a consequence of that core rather than an argument for this framework. Protocol, figures and reproduction steps are on the benchmarks page.
Pick NestRS for the isolation guarantee, and take the performance as given.
Where axum is still the right call
Section titled “Where axum is still the right call”Respect the tool for its job. Reach for axum directly when the application framework has nothing to carry:
- A small proxy or gateway with no data model and no per-tenant authz.
- A single-tenant service where there is no row-level isolation to enforce and no masking to apply.
- A library or embedded component that should stay a thin dependency, not pull in a composition model.
NestRS earns its keep the moment you have cross-cutting concerns — authentication, authorization, tenancy, transactions — repeated across many features. That is exactly where hand-wired correctness drifts, and exactly what the framework is built to make structural. If your service does not have that shape, a bare router is the right size — and NestRS is still there the day it does.
Going further
Section titled “Going further”- Why NestRS — the thesis and the six structural properties that follow.
- Security — how row-level filtering, gating, and masking compose.
- The Publish workspace — the users/orgs/posts universe every example uses.
- Fundamentals — Providers — the access graph checked at boot.