Skip to main content

Middleware

Global middleware

Runs for every route, in registration order:

app.use(async (context, next) => {
console.log(context.request.method, context.request.url);
return next();
});

Per-route middleware

@Use attaches middleware to a single route, composed around that route's handler:

import {Use} from "nextjs-nestapi";

@Controller("/hello")
export class HelloController {
@Use(async (context, next) => {
if (!context.request.headers.get("authorization")) {
return context.json({message: "Unauthorized"}, {status: 401});
}
return next();
})
@Get("")
list() {
return {message: "hello world"};
}
}

Real-world examples

1. Authentication + roles

Authenticate once in global middleware by putting the user on context.user, then let the built-in @AuthGuard enforce it per route instead of hand-rolling the check:

app.use(async (context, next) => {
const token = context.request?.headers.get("authorization")?.replace("Bearer ", "");
context.user = token ? await verifyToken(token) : null;
return next();
});
@Controller("/orders")
export class OrderController {
@AuthGuard(["ADMIN"])
@Get("")
list(@CurrentUser() user: {id: string; role: string}) {
return {orders: []};
}
}

2. Request timing + logging

app.use(async (context, next) => {
const start = Date.now();
const response = await next();
console.log(`${context.request.method} ${context.request.url} - ${Date.now() - start}ms`);
return response;
});

Global middleware wraps next(), so it can inspect (or even rewrite) the response on the way back out, not just the request on the way in.

3. CORS headers

app.use(async (context, next) => {
if (context.request.method === "OPTIONS") {
return context.json(null, {
status: 204,
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET,POST,PUT,PATCH,DELETE,OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
},
});
}

const response = await next();
response.headers.set("Access-Control-Allow-Origin", "*");
return response;
});

4. Rate limiting

In-memory, per IP — swap the Map for Redis in a multi-instance deployment:

const hits = new Map<string, {count: number; resetAt: number}>();

app.use(async (context, next) => {
const ip = context.request.headers.get("x-forwarded-for") ?? "unknown";
const now = Date.now();
const entry = hits.get(ip);

if (!entry || now > entry.resetAt) {
hits.set(ip, {count: 1, resetAt: now + 60_000});
} else if (entry.count >= 100) {
return Response.TooManyRequests("Rate limit exceeded, try again later"); // -> HTTP 429
} else {
entry.count += 1;
}

return next();
});

5. API key check

For server-to-server / public API routes, kept separate from user auth:

import {Use, Response} from "nextjs-nestapi";

function requireApiKey() {
return async (context: RouteContext, next: () => Promise<any>) => {
const key = context.request.headers.get("x-api-key");
if (!key || !(await isValidApiKey(key))) {
return Response.Unauthorized("Invalid API key");
}
return next();
};
}

@Controller("/webhooks")
export class WebhookController {
@Use(requireApiKey())
@Post("/stripe")
handleStripe(context: RouteContext) {
return {received: true};
}
}

6. Error boundary

Catch anything a handler throws and turn it into a structured 500 instead of an unhandled-exception page:

app.use(async (context, next) => {
try {
return await next();
} catch (err) {
console.error(err);
return Response.InternalServerError(
process.env.NODE_ENV === "production" ? "Something went wrong" : String(err)
);
}
});

7. Request body size limit

app.use(async (context, next) => {
const length = Number(context.request.headers.get("content-length") ?? 0);
if (length > 5 * 1024 * 1024) {
return Response.BadRequest("Request body too large (max 5MB)");
}
return next();
});

8. Response caching headers

Per route:

@Controller("/posts")
export class PostController {
@Use(async (context, next) => {
const response = await next();
response.headers.set("Cache-Control", "public, max-age=60, stale-while-revalidate=300");
return response;
})
@Get("")
list() {
return {posts: []};
}
}

9. IP allowlist

For internal/admin-only endpoints:

const ALLOWED_IPS = new Set(["10.0.0.1", "10.0.0.2"]);

@Controller("/internal")
export class InternalController {
@Use(async (context, next) => {
const ip = context.request.headers.get("x-forwarded-for");
if (!ip || !ALLOWED_IPS.has(ip)) return Response.Forbidden();
return next();
})
@Get("/metrics")
metrics() {
return {uptime: process.uptime()};
}
}

10. Request ID / correlation ID

Attach one to every request for log tracing across services:

app.use(async (context, next) => {
const requestId = context.request.headers.get("x-request-id") ?? crypto.randomUUID();
const response = await next();
response.headers.set("x-request-id", requestId);
return response;
});

Signature

type Middleware = (
context: RouteContext,
next: () => Promise<NextResponse | Response | any>
) => Promise<NextResponse | Response | any> | NextResponse | Response | any;

Call next() to continue to the next middleware (or the handler itself); return a value directly to short-circuit the chain without running the handler.