pino
Pino is the default production logger for Node.js APIs: fast structured JSON, low overhead, and first-class Fastify integration.
Recipe
Quick-reference recipe card - copy-paste ready.
import pino from "pino";
export const logger = pino({
level: process.env.LOG_LEVEL ?? "info",
redact: ["req.headers.authorization", "password", "cookie"],
base: { service: "orders-api", env: process.env.NODE_ENV },
});
logger.info({ event: "server_start", port: 3000 });# Development only - never in production Dockerfile
npm install -D pino-pretty
LOG_LEVEL=debug node app.js | npx pino-prettyWhen to reach for this:
- Every production Node service (Express, Fastify, Nest, workers)
- Centralized log aggregation (Datadog, CloudWatch, Loki, ELK)
- Correlating requests with
requestIdacross services
Working Example
// src/logger.ts
import pino from "pino";
export function createLogger() {
return pino({
level: process.env.LOG_LEVEL ?? "info",
timestamp: pino.stdTimeFunctions.isoTime,
redact: {
paths: [
"req.headers.authorization",
"req.headers.cookie",
"body.password",
"body.token",
],
censor: "[REDACTED]",
},
formatters: {
level(label) {
return { level: label };
},
},
});
}
// src/middleware/request-context.ts
import type { Request, Response, NextFunction } from "express";
import { randomUUID } from "node:crypto";
import type { Logger } from "pino";
export function requestContext(log: Logger) {
return (req: Request, res: Response, next: NextFunction) => {
const requestId = (req.headers["x-request-id"] as string) ?? randomUUID();
req.log = log.child({ requestId });
res.setHeader("x-request-id", requestId);
const start = Date.now();
res.on("finish", () => {
req.log.info({
event: "request_complete",
method: req.method,
path: req.path,
statusCode: res.statusCode,
durationMs: Date.now() - start,
});
});
next();
};
}// Express usage
import express from "express";
import { createLogger } from "./logger.js";
import { requestContext } from "./middleware/request-context.js";
const log = createLogger();
const app = express();
declare global {
namespace Express {
interface Request {
log: import("pino").Logger;
}
}
}
app.use(requestContext(log));
app.get("/health", (req, res) => {
req.log.debug({ event: "health_check" });
res.json({ status: "ok" });
});Production rules:
- Log to stdout only - the container runtime ships logs
- Never use
pino-prettyin production images - Use child loggers per request, not global string concatenation
- Log events as objects:
{ event: "order_created", orderId }
Fastify Integration
Fastify uses Pino by default:
import Fastify from "fastify";
const app = Fastify({
logger: {
level: "info",
redact: ["req.headers.authorization"],
},
});See Logging with Pino for hooks, serializers, and request IDs.
Comparison
| Logger | Use when | Skip when |
|---|---|---|
| Pino | JSON services, Fastify, high throughput | Legacy apps deeply tied to Winston transports |
| Winston | Existing Winston-only codebase | Greenfield APIs |
| console.log | Local spike only | Any deployed environment |
| Bunyan | Maintaining legacy services | New projects |
FAQs
Should I log errors with logger.error or throw?
Throw for control flow; log at the boundary that handles the error (HTTP error handler, worker catch). Log once with err key: log.error({ err, orderId }, "payment_failed").
How do I log unhandled rejections?
process.on("unhandledRejection", (reason) => {
logger.fatal({ err: reason }, "unhandled_rejection");
process.exit(1);
});Document exit policy per service - Kubernetes will restart the pod.
PII in logs?
Redact at config. Prefer opaque IDs (userId) over emails and names. If GDPR applies, define retention and scrubbing in the log platform, not in app code alone.
Related
- Logging with Pino - Fastify-specific patterns
- Essential Libraries Basics - why Pino is default
- Observability Basics - traces + logs correlation
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.