HTTP Logging with Morgan
Morgan is Express middleware dedicated to one job: logging incoming HTTP requests. For every request it writes a one-line access log — method, URL, status code, response size, response time — in formats modelled on the classic Apache/nginx access logs. It's the simplest way to get "who hit what and what happened" visibility. This page covers the predefined formats, custom tokens, writing to files, conditional skipping, and the important distinction between access logging (Morgan) and application logging (Winston/Pino).
Setup and predefined formats
npm install morgan
import express from 'express'
import morgan from 'morgan'
const app = express()
// 'dev' — concise, colour-coded by status, great for development:
app.use(morgan('dev'))GET /api/orders/9981 200 12.480 ms - 256 POST /api/users 201 45.110 ms - 89 GET /api/missing 404 1.230 ms - 142
Format | Output style | Use |
|---|---|---|
| Concise, colour-coded status | Development terminal |
| Apache combined (incl. referrer, user-agent) | Production access logs |
| Apache common log format | Standard access logs |
| Shorter than default | Less verbose output |
| Minimal — method, url, status, size, time | Low-noise logging |
The combined format and custom tokens
app.use(morgan('combined'))10.0.0.5 - - [21/Jun/2026:09:30:02 +0000] "GET /api/orders/9981 HTTP/1.1" 200 256 "https://app.example.com" "Mozilla/5.0 (Macintosh; ...)"
Define custom tokens and a custom format string
// A token to log an authenticated user id set by your auth middleware:
morgan.token('user', (req) => req.user?.id ?? 'anon')
// A token for a correlation/request id:
morgan.token('reqId', (req) => req.id)
app.use(
morgan(':reqId :user :method :url :status :res[content-length] - :response-time ms'),
)abc123 42 GET /api/orders/9981 200 256 - 12.480 ms
Writing access logs to a file or stream
import fs from 'node:fs'
import path from 'node:path'
// Append access logs to a file via a write stream:
const accessLogStream = fs.createWriteStream(path.join('logs', 'access.log'), {
flags: 'a', // append, don't truncate
})
app.use(morgan('combined', { stream: accessLogStream }))Pipe Morgan INTO your structured logger instead of a file
import { logger } from './logger.js'
// Send each access line through Winston/Pino so it lands in one place:
app.use(
morgan('combined', {
stream: { write: (message) => logger.info(message.trim()) },
}),
)Skipping noise
// Only log errors in production (skip the 2xx/3xx noise):
app.use(
morgan('combined', {
skip: (req, res) => res.statusCode < 400,
}),
)
// Or skip health-check and static-asset spam:
app.use(
morgan('dev', {
skip: (req) => req.url === '/health' || req.url.startsWith('/static'),
}),
)