NestJS comes with a built in text-based logger that’s quite useful but for production grade logging you can implement your own system fairly easily. Express apps typically use winston, morgan or a combination of the two and there are several guides on how to implement this integration. While there are many guides available, finding one that integrates both for a NestJS application can be challenging. This guide demonstrates one approach to implementing this integration.
If you haven’t used these libraries before winston is a very flexible logger that supports logging to multiple transports (output streams, basically), while morgan is specifically an HTTP request logger that’s enabled as middleware in your app.
npm i winston morgan @types/morgan
Following the standard NestJS pattern, implement your logger as a module that can be injected as a
dependency wherever needed. Per the docs,
to enable dependency injection for your custom logger, create a class that implements LoggerService
and register that class as a provider.
src/logger/logger.ts
import {
createLogger,
transports,
format,
Logger as InternalLogger,
} from 'winston'
@Injectable
export class WinstonLogger implements LoggerService {
private readonly logger: InternalLogger
constructor() {
this.logger = createLogger({
level: 'debug',
format: format.json(),
transports: [new transports.Console()],
})
}
log(message: any, ...optionalParams: any[]) {
this.logger.info(message, optionalParams)
}
error(message: any, ...optionalParams: any[]) {
this.logger.error(message, optionalParams)
}
warn(message: any, ...optionalParams: any[]) {
this.logger.warn(message, optionalParams)
}
debug?(message: any, ...optionalParams: any[]) {
this.logger.debug(message, optionalParams)
}
verbose?(message: any, ...optionalParams: any[]) {
this.logger.verbose(message, optionalParams)
}
fatal?(message: any, ...optionalParams: any[]) {
this.logger.error(message, optionalParams)
}
setLogLevels?(levels: LogLevel[]) {}
}
This creates a winston logger with a basic configuration. Note that this should be customized based
on production or dev environments. Extending LoggerService
requires providing an implementation
for log methods, which you can pass through to winston. To use morgan on top of this for standard HTTP logging, add the following.
src/logger/httpLogger.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common'
import { Request, Response, NextFunction } from 'express'
import { WinstonLogger } from './logger'
import * as morgan from 'morgan'
@Injectable()
export class HTTPLoggerMiddleware implements NestMiddleware {
constructor(private readonly logger: WinstonLogger) {}
use(req: Request, res: Response, next: NextFunction) {
morgan('tiny', {
skip: (req, res) => res.statusCode < 400 || res.statusCode >= 500,
stream: {
write: (message) => {
this.logger.warn(message.trim())
},
},
})(req, res, next)
morgan('tiny', {
skip: (req, res) => res.statusCode < 200 || res.statusCode >= 400,
stream: {
write: (message) => {
this.logger.log(message.trim())
},
},
})(req, res, next)
morgan('tiny', {
skip: (req, res) => res.statusCode < 500 || res.statusCode >= 600,
stream: {
write: (message) => {
this.logger.error(message.trim())
},
},
})(req, res, next)
}
}
HTTPLoggerMiddleware
extends NestMiddleware
and passes the request and response through morgan. This implementation creates several morgan logger functions based on the HTTP status code of the
response and uses a specific log function. While optional, this approach is useful if you’ve
customized your winston logger to log successes and failures to separate files, or if you’ve enabled
the colorization functionality. Note that this also uses a predefined log format, tiny
, but that can also be customized if needed.
To complete this implementation, update your AppModule
to use the new middleware.
@Module({
imports: [
// Other modules
LoggerModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(HTTPLoggerMiddleware).forRoutes('*')
}
}
and in your main.ts
, update the app
instance to use your new logger.
const app = await NestFactory.create(AppModule, { bufferLogs: true })
app.useLogger(new WinstonLogger())
Note that bufferLogs
is set to true so that service initialization logs that are written before
WinstonLogger
is created are still available. If WinstonLogger
creation fails at this point NestJS falls
back to the default logger.