Middleware
NestRS has no middleware primitive — Guards and Interceptors cover the two roles. A short guide for readers looking for it.
NestRS doesn’t have a middleware primitive. Some backend frameworks ship one for cross-cutting work; NestRS splits the responsibility so the shape of each layer is explicit:
- Gate-and-context work lives in Guards —
pre-handler, can short-circuit with a response, can attach typed
context the handler reads back via
Ctx<T>. - Wrap-the-handler work lives in Interceptors — observe both sides, install ambient state, transform the response.
If you’re looking for a place to put work the word “middleware” usually covers — logging, request id, rate limiting, auth, opening a transaction — the table below maps the role to its NestRS home.
What the word usually covers, and where it lands in NestRS
Section titled “What the word usually covers, and where it lands in NestRS”| Role | NestRS primitive | Why |
|---|---|---|
| Logging the request | Interceptor | Needs both sides (start time, end time, status) |
Adding a X-Cache-Status header to the response | Interceptor | Touches the response |
| Authenticating the bearer token | Guard (AuthnGuard) | Short-circuit on 401, attach Claims for the handler |
| Rate-limiting | Guard | Short-circuit on 429 before any work runs |
| Parsing/normalizing the body | Pipe at the boundary | Pure transform, no DI |
| Opening a database transaction | Interceptor (DbContext, auto-mounted) | Wraps the handler, commits/rolls back on exit |
| Stamping a request / correlation id | Nothing to write | Every unit of work already runs under a W3C trace |
| CORS, security headers | poem middleware, configured on HttpConfig | Lower-level than a Guard/Interceptor |
The split is on purpose: “gate” and “wrap” are different shapes, the framework names them differently, and the request-layer ordering reflects the difference.
“But I want a middleware”
Section titled ““But I want a middleware””Three shapes cover it. Pick the one that matches.
“I want to see every request and add a header to the response.”
Section titled ““I want to see every request and add a header to the response.””That’s an interceptor.
#[injectable]#[derive(Default)]pub struct NoIndex;
impl Layer for NoIndex {}
#[async_trait]impl Interceptor for NoIndex { async fn intercept(&self, req: Request, next: Next<'_>) -> Result<Response> { let mut resp = next.run(req).await?; resp.headers_mut() .insert("x-robots-tag", HeaderValue::from_static("noindex")); Ok(resp) }}Bind it globally — either by adding #[interceptor] on the struct
(auto-mount on every route) or via
App::builder().use_interceptors_global([interceptor::<NoIndex>()])
in main.
Do not write this one to stamp a correlation id: the framework already runs
every request under a trace and reports it as
traceresponse. A hand-stamped id correlates with nothing the logs, the access
line or a queued job are filed under.
“I want to validate the request before any handler runs and block it if invalid.”
Section titled ““I want to validate the request before any handler runs and block it if invalid.””That’s a guard. Gate the request, return Err(Denial) on rejection.
use nest_rs::guards::prelude::*;use nest_rs::http::poem::Request as HttpRequest;
#[injectable]#[derive(Default)]pub struct RequireApiKey;
impl Layer for RequireApiKey {}
#[async_trait]impl Guard for RequireApiKey { async fn check_http(&self, req: &mut HttpRequest) -> Result<(), Denial> { if req.headers().get("x-api-key").is_some() { Ok(()) } else { Err(Denial::unauthorized("missing key")) } }}
impl HttpGuard for RequireApiKey {}Bind with #[use_guards(RequireApiKey)] per controller or per handler,
or globally via
App::builder().use_guards_global([guard::<RequireApiKey>()]).
“I want to attach something to the request so the handler can read it.”
Section titled ““I want to attach something to the request so the handler can read it.””A guard does that too. The guard borrows the request mutably:
#[async_trait]impl Guard for AuthnGuard { async fn check_http(&self, req: &mut HttpRequest) -> Result<(), Denial> { let claims = verify_bearer(req).map_err(|_| Denial::unauthorized("invalid token"))?; req.extensions_mut().insert(claims); Ok(()) }}
impl HttpGuard for AuthnGuard {}
// In the handler:async fn me(&self, auth: Ctx<Claims>) -> Json<User> { /* ... */ }What about the raw poem middleware trait?
Section titled “What about the raw poem middleware trait?”nest-rs-http builds on poem, and poem has its
own Middleware trait. The framework exposes the named categories
(Guard / Interceptor / Filter) layered over it. CORS, request-size
limits, body-decoding — those things live on HttpConfig and are
applied as poem middleware by the transport, not as NestRS Guards or
Interceptors.
nest-rs-http owns the poem endpoint, which is what makes every layer
go through DI and the access graph — portable across controllers and
checkable at boot. A third-party poem::Middleware ports to an
Interceptor in a few lines and gains both: it wraps the handler the
same way, with DI and the access graph on top. The lower-level knobs
poem middleware is often reached for — CORS, TLS, body-size limits,
request timeout — are already HttpConfig fields.
Going further
Section titled “Going further”- Guards — gate and attach context.
- Interceptors — wrap and observe.
- Pipes — pure input transforms.
- HTTP / configuration — CORS, TLS, the
framework
Server:header.