Skip to content

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”
RoleNestRS primitiveWhy
Logging the requestInterceptorNeeds both sides (start time, end time, status)
Adding X-Request-Id to the responseInterceptorTouches the response
Authenticating the bearer tokenGuard (AuthGuard)Short-circuit on 401, attach Claims for the handler
Rate-limitingGuardShort-circuit on 429 before any work runs
Parsing/normalizing the bodyPipe at the boundaryPure transform, no DI
Opening a database transactionInterceptor (DbContext, auto-mounted)Wraps the handler, commits/rolls back on exit
CORS, security headerspoem middleware, configured on HttpConfigLower-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.

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> { /* ... */ }

See Guards / Attach context.

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.