Using Express middleware in NestJS
When you move from vanilla Express to NestJS, you might wonder whether you need to rewrite all your middleware. The good news is that Express middleware still works in NestJS—you just need to understand how to integrate it properly. I've spent countless hours helping teams migrate legacy middleware into NestJS applications, and the pattern is straightforward once you know your options.
Understanding middleware scope in NestJS
NestJS runs on top of Express (or Fastify), so the underlying middleware system is still there. However, NestJS adds layers on top—dependency injection, decorators, modules—that change how you think about middleware. Before diving into the three main approaches, let's clarify the scope problem.
In Express, middleware runs in the order it's registered and affects all downstream routes. In NestJS, you have finer control: you can apply middleware globally to every route, to specific modules, or to specific route handlers. This flexibility is powerful, but it requires deliberate choices about where to register your middleware.
Using app.use() for global middleware
The simplest approach is Express middleware in its native form. If you have middleware that doesn't need dependency injection—like CORS, body parsing, compression, or request logging—you can register it globally with app.use() in your main.ts file.
This is exactly what you'd do in Express. NestJS gives you the underlying Express app instance through useStaticAssets(), setViewEngine(), and similar methods on the Nest application, but for raw middleware, you access the underlying Express app.
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as compression from 'compression';
import * as helmet from 'helmet';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Apply global Express middleware
app.use(compression());
app.use(helmet());
// Custom logging middleware (raw Express style)
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
console.log(`${req.method} ${req.path} - ${res.statusCode} (${duration}ms)`);
});
next();
});
await app.listen(3000);
}
bootstrap();This works, and it's useful for cross-cutting concerns that genuinely don't need the NestJS dependency container. However, there's a limitation: this middleware has no access to NestJS services, providers, or configuration. If you need to inject a logger, database connection, or configuration service, you're out of luck.
NestMiddleware classes for dependency injection access
This is where NestMiddleware comes in. When you need your middleware to use NestJS dependency injection, you implement the NestMiddleware interface. This allows your middleware to inject services and work within the NestJS ecosystem while still executing the middleware logic.
Here's a practical example: logging with a NestJS logger service instead of console.log, and tracking request metadata in a database.
import { Injectable, NestMiddleware, Logger } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { RequestLogService } from './request-log.service';
@Injectable()
export class RequestLoggingMiddleware implements NestMiddleware {
private readonly logger = new Logger('Requests');
constructor(private readonly requestLogService: RequestLogService) {}
use(req: Request, res: Response, next: NextFunction) {
const start = Date.now();
const { method, path, ip } = req;
res.on('finish', () => {
const duration = Date.now() - start;
const statusCode = res.statusCode;
// Use injected service to log to database
this.requestLogService.logRequest({
method,
path,
statusCode,
duration,
ip,
timestamp: new Date(),
});
this.logger.debug(
`${method} ${path} - ${statusCode} (${duration}ms)`
);
});
next();
}
}The key difference: this middleware is a class decorated with @Injectable(), it accepts dependencies in its constructor, and it implements the use() method with the standard Express signature. You register it in your module, not in main.ts.
Using configure(consumer) to target specific routes
The real power of NestMiddleware shows up when you use the configure() method in your module. This method lets you register middleware for specific routes, specific HTTP methods, or specific route patterns—all in one place where the logic lives.
Let me show you a real-world scenario: you want request logging on all routes except health checks, and you want authentication middleware only on protected routes.
import {
Module,
MiddlewareConsumer,
RequestMethod,
NestModule,
} from '@nestjs/common';
import { RequestLoggingMiddleware } from './middleware/request-logging.middleware';
import { AuthenticationMiddleware } from './middleware/authentication.middleware';
import { UsersController } from './users/users.controller';
import { HealthController } from './health/health.controller';
@Module({
controllers: [UsersController, HealthController],
providers: [RequestLogService],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
// Apply logging to all routes except /health
consumer
.apply(RequestLoggingMiddleware)
.forRoutes({ path: '*', method: RequestMethod.ALL })
.exclude({ path: 'health', method: RequestMethod.GET });
// Apply authentication only to user routes
consumer
.apply(AuthenticationMiddleware)
.forRoutes(UsersController);
}
}This is much cleaner than manually adding middleware in Express. The consumer API lets you chain methods: apply() registers the middleware class or classes, forRoutes() specifies where it applies (controller class, specific route path, or route object with method), and exclude() carves out exceptions.
Notice that you can pass multiple middleware classes to apply() and they'll execute in order. You can also call consumer.apply().forRoutes() multiple times in the same configure() method to build a more complex middleware graph.
Prefer guards and interceptors for Nest-aware logic
Here's where I need to be blunt: just because you can use middleware doesn't mean you should for everything. NestJS-native constructs like guards and interceptors are often a better choice for business logic.
Middleware runs early in the request lifecycle, before route handlers. This means it can't easily access route parameters or metadata. Guards run after middleware but before handlers and have full context about which handler will run. Interceptors run around the handler and can transform both request and response.
For example, if you're doing authentication, use a guard, not middleware. Guards can check route metadata, use dependency injection cleanly, and throw HTTP exceptions that NestJS knows how to handle.
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { Request } from 'express';
import { AuthService } from './auth.service';
@Injectable()
export class JwtGuard implements CanActivate {
constructor(private readonly authService: AuthService) {}
async canActivate(context: ExecutionContext): Promise {
const request = context.switchToHttp().getRequest();
const token = request.headers.authorization?.split(' ')[1];
if (!token) {
throw new UnauthorizedException('No token provided');
}
try {
const user = await this.authService.validateToken(token);
request.user = user;
return true;
} catch (error) {
throw new UnauthorizedException('Invalid token');
}
}
}This guard does the same job as an authentication middleware would, but it's declarative (you apply it with @UseGuards(JwtGuard)), it has access to the ExecutionContext (which includes route information), and it integrates seamlessly with NestJS exception handling.
Use middleware for truly cross-cutting concerns that don't need route or handler context: logging, CORS, compression, body parsing. Use guards for authorization and authentication checks tied to specific routes. Use interceptors to transform data or handle response formatting.
Common mistakes
Over the years, I've seen three patterns that trip up developers migrating to NestJS:
- Forgetting to implement NestModule: Your module needs to implement the
NestModuleinterface if you want to use theconfigure()method. Without it, you'll get a compilation error. - Registering NestMiddleware in main.ts: NestMiddleware classes must be registered in a module's
configure()method, not viaapp.use(). If you try to pass a NestMiddleware class toapp.use(), it won't work because NestJS won't instantiate it or inject dependencies. - Overthinking simple cases: If your middleware doesn't need dependency injection, just use
app.use()in main.ts. Creating a NestMiddleware class for compression or CORS adds unnecessary boilerplate. Keep it simple.
Key takeaways
- Express middleware works natively in NestJS via
app.use()for global, DI-free middleware like compression, CORS, and helmet. - Implement
NestMiddlewarewhen you need dependency injection, then register it in your module'sconfigure()method. - Use the
MiddlewareConsumerAPI to target specific routes, controllers, or HTTP methods, and chainapply(),forRoutes(), andexclude()to build your middleware graph. - Prefer guards for authentication and authorization, and interceptors for request/response transformation. Middleware is best for truly agnostic concerns like logging and request timing.
- NestMiddleware runs in the order you register it and before guards and interceptors, so plan your middleware strategy early in your module design.






Comments
Loading comments…