Request Logging with Morgan

Morgan writes one line per completed response. It is ordinary middleware: it records the start time, hooks res for the finish event, and calls next() at once, so the line appears after the handler replied.

morgan-app.js — a development logger and an error-only access logJavaScript
import express from 'express';
import morgan from 'morgan';
const app = express();
app.use(morgan('dev'));                                  // colored one-liner, stdout
app.use(morgan('combined', { skip: (req, res) => res.statusCode < 400 }));
app.get('/api/books', (req, res) => res.json([{ id: 1, title: 'Dune' }]));
app.get('/api/books/:id', (req, res) => req.params.id === '1'
  ? res.json({ id: 1, title: 'Dune' })
  : res.status(404).json({ error: 'not found' }));
app.listen(3101, () => console.log('listening on 3101, pid', process.pid));

Three requests — a list, a hit and a miss — produce four lines, because the 404 matches both loggers:

Output of 52
GET /api/books 200 2.366 ms - 25
GET /api/books/1 200 0.592 ms - 23
GET /api/books/99 404 0.365 ms - 21
::1 - - [17/Sep/2026:17:57:25 +0000] "GET /api/books/99 HTTP/1.1" 404 21 "-" "curl/8.9.1"

The trailing number is the body's byte length; dev colors the status on a TTY, so never point it at a file. combined is the Apache format log processors already parse, and morgan.token('id', (req) => req.id ?? '-') registers a token for a custom format. Morgan's limits show up the day you ship: the output is text a platform must re-parse, it has no levels to change at runtime, and it never sees errors thrown inside handlers.