Skip to content

Morgan Guide (80-20)#

The 20% you need to know that covers 80% of use cases.

Installation#

npm install morgan

Basic Setup#

const express = require('express')
const morgan = require('morgan')

const app = express()

// Add morgan middleware
app.use(morgan('dev'))

app.get('/api/users', (req, res) => {
  res.json({ users: [] })
})

app.listen(3000)

Predefined Formats#

dev (Development)#

Color-coded, concise output for development.

app.use(morgan('dev'))

Output:

GET /api/users 200 12.345 ms - 150

Colors: - Green: 2xx success - Cyan: 3xx redirect - Yellow: 4xx client error - Red: 5xx server error

combined (Production)#

Standard Apache combined log format.

app.use(morgan('combined'))

Output:

::1 - - [26/Nov/2025:15:30:15 +0000] "GET /api/users HTTP/1.1" 200 1234 "-" "Mozilla/5.0"

common#

Standard Apache common log format.

app.use(morgan('common'))

Output:

::1 - - [26/Nov/2025:15:30:15 +0000] "GET /api/users HTTP/1.1" 200 1234

tiny#

Minimal output.

app.use(morgan('tiny'))

Output:

GET /api/users 200 1234 - 12.345 ms

Custom Format#

Create your own format using tokens:

// Custom format: method url status response-time
app.use(morgan(':method :url :status :response-time ms'))

Output:

GET /api/users 200 12.345 ms

Available Tokens#

Token Description Example
:method HTTP method GET
:url Request URL /api/users
:status Response status 200
:response-time Response time (ms) 12.345
:date Current date/time Wed, 26 Nov 2025 15:30:15 GMT
:http-version HTTP version 1.1
:remote-addr Client IP ::1
:user-agent User agent Mozilla/5.0...
:referrer Referrer URL -

Custom Tokens#

Define your own tokens:

// Custom token for user ID
morgan.token('user-id', (req, res) => {
  return req.user?.id || 'anonymous'
})

// Use in format
app.use(morgan(':method :url :status - User: :user-id'))

Output:

GET /api/users 200 - User: 123

Conditional Logging#

Log only errors (4xx, 5xx):

app.use(morgan('combined', {
  skip: (req, res) => res.statusCode < 400
}))

Log only successful requests:

app.use(morgan('dev', {
  skip: (req, res) => res.statusCode >= 400
}))

Logging to File#

Write to File Stream#

const fs = require('fs')
const path = require('path')

// Create write stream
const accessLogStream = fs.createWriteStream(
  path.join(__dirname, 'access.log'),
  { flags: 'a' } // append mode
)

// Log to file
app.use(morgan('combined', { stream: accessLogStream }))

Separate Error Logs#

const accessLogStream = fs.createWriteStream(
  path.join(__dirname, 'access.log'),
  { flags: 'a' }
)

const errorLogStream = fs.createWriteStream(
  path.join(__dirname, 'error.log'),
  { flags: 'a' }
)

// Success logs
app.use(morgan('combined', {
  stream: accessLogStream,
  skip: (req, res) => res.statusCode >= 400
}))

// Error logs
app.use(morgan('combined', {
  stream: errorLogStream,
  skip: (req, res) => res.statusCode < 400
}))

Environment-Based Config#

Different logging for dev vs production:

const morgan = require('morgan')

if (process.env.NODE_ENV === 'production') {
  // Production: detailed logs to file
  const accessLogStream = fs.createWriteStream(
    path.join(__dirname, 'access.log'),
    { flags: 'a' }
  )
  app.use(morgan('combined', { stream: accessLogStream }))
} else {
  // Development: colored console output
  app.use(morgan('dev'))
}

TypeScript Usage#

import express, { Express } from 'express'
import morgan from 'morgan'
import fs from 'fs'
import path from 'path'

const app: Express = express()

// Development
if (process.env.NODE_ENV !== 'production') {
  app.use(morgan('dev'))
}

// Production
if (process.env.NODE_ENV === 'production') {
  const accessLogStream = fs.createWriteStream(
    path.join(__dirname, '../logs/access.log'),
    { flags: 'a' }
  )

  app.use(morgan('combined', { stream: accessLogStream }))
}

Custom Format with Colors (Dev)#

const morgan = require('morgan')
const chalk = require('chalk') // npm install chalk

morgan.token('colored-status', (req, res) => {
  const status = res.statusCode
  const color = status >= 500 ? 'red'
    : status >= 400 ? 'yellow'
    : status >= 300 ? 'cyan'
    : 'green'

  return chalk[color](status)
})

app.use(morgan(':method :url :colored-status :response-time ms'))

Log Request Body#

morgan.token('body', (req) => JSON.stringify(req.body))

app.use(express.json()) // Parse body first
app.use(morgan(':method :url :status - :body'))

Security

Be careful logging request bodies - they may contain sensitive data like passwords!

Skip Static Files#

Don't log requests for static assets:

app.use(morgan('combined', {
  skip: (req, res) => req.url.startsWith('/static')
}))

Best Practices#

  1. Use 'dev' in development - Easy to read, color-coded
  2. Use 'combined' in production - Standard format, detailed
  3. Log to files in production - Don't rely on console logs
  4. Rotate log files - Use tools like rotating-file-stream
  5. Skip logging health checks - Reduce noise
  6. Never log sensitive data - Passwords, tokens, etc.

Example: Production Setup#

import express from 'express'
import morgan from 'morgan'
import fs from 'fs'
import path from 'path'

const app = express()

// Ensure logs directory exists
const logsDir = path.join(__dirname, '../logs')
if (!fs.existsSync(logsDir)) {
  fs.mkdirSync(logsDir)
}

// Create write streams
const accessLogStream = fs.createWriteStream(
  path.join(logsDir, 'access.log'),
  { flags: 'a' }
)

const errorLogStream = fs.createWriteStream(
  path.join(logsDir, 'error.log'),
  { flags: 'a' }
)

// Success logs (2xx, 3xx)
app.use(morgan('combined', {
  stream: accessLogStream,
  skip: (req, res) => res.statusCode >= 400
}))

// Error logs (4xx, 5xx)
app.use(morgan('combined', {
  stream: errorLogStream,
  skip: (req, res) => res.statusCode < 400
}))

// Skip health check endpoint
app.use(morgan('dev', {
  skip: (req, res) => req.url === '/health'
}))

app.listen(3000)

Quick Setup

For most apps, morgan('dev') in development and morgan('combined') in production is all you need!