pino
Pino is the default production logger for Node.js APIs: fast structured JSON, low overhead, and first-class Fastify integration.
Search across all documentation pages
Pino is the default production logger for Node.js APIs: fast structured JSON, low overhead, and first-class Fastify integration.
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:
requestId across services// 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:
pino-pretty in production images{ event: "order_created", orderId }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.
| 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 |
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").
process.on("unhandledRejection", (reason) => {
logger.fatal({ err: reason }, "unhandled_rejection");
process.exit(1);
});Document exit policy per service - Kubernetes will restart the pod.
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.
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.
Reviewed by Chris St. John·Last updated Jul 16, 2026