console.log works fine on a local machine, but a real running server needs more: log levels, timestamps, and output that can actually be searched later — usually across thousands of lines a day.
| Problem | What a real logger solves |
|---|---|
| No severity level | An error and a routine info message look identical |
| No timestamp by default | Impossible to know when something happened after the fact |
| Plain text only | Hard to search or filter at scale — structured (JSON) logs can be queried |
| Always prints, even in production | A real logger can be configured to skip verbose logs outside development |
import winston from 'winston'
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [new winston.transports.Console()],
})
logger.info('Server started', { port: 3000 })
logger.error('Database connection failed', { error: err.message }){"level":"info","message":"Server started","port":3000,"timestamp":"2026-09-03T10:00:00.000Z"}| Level | Use for |
|---|---|
| error | Something failed and needs attention |
| warn | Something unexpected, but the app kept working |
| info | Normal, notable events — a server starting, a user signing up |
| debug | Detailed information, usually only enabled during active troubleshooting |
morgan is Express-specific middleware (from the earlier Middleware lesson) that automatically logs every incoming request.
import morgan from 'morgan'
app.use(morgan('combined'))
// Logs: method, URL, status code, response time, for every request automaticallyWhat never belongs in a log
Never log a password, a full credit card number, or an API secret — even at the debug level. A log file is often less carefully protected than the database itself, and logs frequently get shipped to third-party monitoring tools.