Middleware
A middleware is a function that wraps the whole application: it gets the Request and a next() that runs the rest, and returns the Response. Use it for things that apply to every request: access logs, response headers, CORS, rate limits, request ids.
import { createHttpApplication, type Middleware } from "@blixis-io/http";
const accessLog: Middleware = async (request, next) => { const started = performance.now(); const response = await next(); console.log(request.method, new URL(request.url).pathname, response.status, `${Math.round(performance.now() - started)}ms`); return response;};
const app = await createHttpApplication(AppModule, { middleware: [accessLog] });The same option works on createFetchHandler. There is no app.use(): the chain is the array, so the order is visible in one place.
Middleware or interceptor?
Section titled “Middleware or interceptor?”| Middleware | Interceptor | |
|---|---|---|
| Applies to | every request | one controller or route |
Sees 404, 405, malformed path, mounted routes |
yes | no |
Sees a guard’s 403 |
yes | no, runs after guards |
| Dependency injection | no, a plain function | yes, a class |
| Knows the matched route | no | yes, ExecutionContext |
The first entry in the array is the outermost. For [a, b], a request runs a before b before the application, and the response comes back through b then a.
a > b > timeout > routing > guards > interceptors > handlerThe chain sits outside requestTimeout: the deadline covers the application, so a middleware that never calls next() or never returns is not timed out for you. A request that times out is a 504 response that the middleware sees like any other.
What a middleware does
Section titled “What a middleware does”- Call
next()and return its response, changed or not. To add headers, usewithResponseHeaders(response, { name: value })rather thanresponse.headers.set(): some responses have immutable headers (Response.redirect(), one returned byfetch()), andset()throws on those.next()can be called once; a second call is an error. - Answer itself, without calling
next(): a429from a rate limiter, a preflight204from CORS. Nothing downstream runs. - Hand on a changed request:
next(new Request(request, { headers })). Without an argument the current request goes on. - Throw. An
HttpException(throw new UnauthorizedException("no token")) answers with that exception’s problem+json. Any other error is logged and answers a generic500, with the message kept from the client. A middleware that returns something other than aResponseis also a500.
A failure anywhere further in (a controller error, a mounted handler that throws, or another middleware that throws) reaches the middleware outside it as a response, never as a rejection from next(). So next() needs no try/catch, and a CORS or logging middleware placed first still sees, and can decorate, the 429 a rate limiter threw or the 500 a broken middleware caused.
Request context
Section titled “Request context”The chain runs inside a RequestContext scope, and the guards and the controller then use that same scope. A value a middleware sets (a request id, say) is the one a guard or service reads, and no other request sees it. A request made from inside a handler (app.handle() for a sub-request) gets a scope of its own.
What a middleware sees of the response
Section titled “What a middleware sees of the response”The Response as it was created, not the end of its body. A streamed body is still being produced when next() resolves, so a middleware can set headers and read the status, but a latency it measures is the time to the response head, not to the last byte. Don’t read the body in a middleware unless you intend to replace it: a body can only be read once.
What it doesn’t do
Section titled “What it doesn’t do”It doesn’t know which route matched, doesn’t take part in dependency injection, and ships no CORS, security-header, rate-limit or logging middleware of its own. Those are small functions you write, or that a package provides, using this option.