Middleware
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 X-Request-Id to the response | Interceptor | Touches the response |
| Authenticating the bearer token | Guard (AuthGuard) | 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 |
| 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””You probably want one of three things. 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 RequestId;
#[async_trait]impl Interceptor for RequestId { async fn intercept(&self, req: Request, next: Next<'_>) -> Result<Response> { let id = uuid::Uuid::now_v7().to_string(); let mut resp = next.run(req).await?; resp.headers_mut().insert("x-request-id", id.parse().unwrap()); 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::<RequestId>()])
in main.
”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")) } }}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 AuthGuard { 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(()) }}
// 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.
If a third-party crate ships a poem::Middleware, there’s no public
hook to bolt it directly onto the transport — nest-rs-http doesn’t
expose the underlying poem endpoint. Reach for the framework’s own
primitives instead: an Interceptor wraps the handler the same way a
poem::Middleware would, with DI and the access graph on top. The
lower-level knobs poem middleware is often used for — CORS, TLS,
body-size limits, request timeout — live on HttpConfig.
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.