DEV Community

Ansh Sheladiya
Ansh Sheladiya

Posted on

Logging Best Practices in Node.js: Build Observable and Reliable APIs

Logging is one of the simplest ways to understand what a Node.js application is doing in production. When an API fails, a background job gets stuck, or a request suddenly becomes slow, useful logs can turn a difficult debugging session into a straightforward investigation.

However, logging everything is not the same as logging well. Production applications need structured, consistent, searchable, and security-conscious logs that provide enough context without creating unnecessary noise or exposing sensitive information.

Build Structured and Production-Ready Logging in Node.js

A good logging strategy starts with consistent log levels. debug is useful during development, info describes normal application behavior, warn highlights unusual situations, and error represents failures that require attention. Using predictable levels makes it easier to filter logs in platforms such as CloudWatch, Datadog, Elasticsearch, or other observability systems.

Structured logging is another important practice for Node.js applications. Instead of generating unstructured messages such as User login failed, include useful fields like request ID, user ID, operation name, status code, and duration. JSON logs are particularly useful because monitoring systems can parse and search individual fields without relying on fragile text patterns.

Logs should also provide request-level context. A request ID or correlation ID allows developers to follow a request across middleware, database operations, services, and external APIs. The following example implements a small structured logger, request context, log levels, sensitive-data redaction, error handling, and request timing using only built-in Node.js modules.

Another important rule is to avoid logging secrets. Passwords, access tokens, authorization headers, API keys, payment information, and other sensitive values should be removed or masked before reaching your logging system. Finally, avoid excessive logs inside high-frequency code paths because noisy logs increase storage costs and can make important production failures harder to identify.

const http = require("http");
const crypto = require("crypto");

// Define log levels so production output can be filtered consistently.
const LOG_LEVELS = {
  debug: 10,
  info: 20,
  warn: 30,
  error: 40
};

// Configure the minimum level from the environment.
const configuredLevel = process.env.LOG_LEVEL || "info";

function shouldLog(level) {
  return LOG_LEVELS[level] >= LOG_LEVELS[configuredLevel];
}

// Remove fields that should never be written to application logs.
function redact(value) {
  if (!value || typeof value !== "object") {
    return value;
  }

  const sensitiveFields = new Set([
    "password",
    "token",
    "accessToken",
    "refreshToken",
    "authorization",
    "apiKey"
  ]);

  const output = Array.isArray(value) ? [] : {};

  for (const [key, currentValue] of Object.entries(value)) {
    if (sensitiveFields.has(key)) {
      output[key] = "[REDACTED]";
      continue;
    }

    output[key] =
      currentValue && typeof currentValue === "object"
        ? redact(currentValue)
        : currentValue;
  }

  return output;
}

function log(level, message, metadata = {}) {
  if (!shouldLog(level)) {
    return;
  }

  const entry = {
    timestamp: new Date().toISOString(),
    level,
    message,
    ...redact(metadata)
  };

  // JSON output is easy for log aggregation systems to parse and search.
  console.log(JSON.stringify(entry));
}

function createRequestId() {
  return crypto.randomUUID();
}

const server = http.createServer(async (req, res) => {
  const requestId = req.headers["x-request-id"] || createRequestId();
  const startedAt = process.hrtime.bigint();

  // Return the correlation ID so clients can report it when debugging issues.
  res.setHeader("x-request-id", requestId);

  log("info", "Incoming HTTP request", {
    requestId,
    method: req.method,
    path: req.url
  });

  try {
    // Simulate application work that could represent a database or API call.
    await new Promise((resolve) => setTimeout(resolve, 120));

    if (req.url === "/health") {
      log("debug", "Health check completed", { requestId });

      res.writeHead(200, { "content-type": "application/json" });
      res.end(JSON.stringify({ status: "ok" }));
    } else if (req.url === "/error") {
      throw new Error("Simulated application failure");
    } else {
      log("info", "Route processed successfully", {
        requestId,
        route: req.url
      });

      res.writeHead(200, { "content-type": "application/json" });
      res.end(JSON.stringify({
        message: "Request processed",
        requestId
      }));
    }
  } catch (error) {
    // Log the error with context while avoiding sensitive request data.
    log("error", "Request failed", {
      requestId,
      error: {
        name: error.name,
        message: error.message,
        stack: error.stack
      }
    });

    res.writeHead(500, { "content-type": "application/json" });
    res.end(JSON.stringify({
      error: "Internal server error",
      requestId
    }));
  } finally {
    const durationMs = Number(process.hrtime.bigint() - startedAt) / 1e6;

    // Request duration helps identify slow endpoints and performance regressions.
    log("info", "Request completed", {
      requestId,
      method: req.method,
      path: req.url,
      durationMs: Number(durationMs.toFixed(2))
    });
  }
});

const PORT = Number(process.env.PORT) || 3000;

server.listen(PORT, () => {
  log("info", "HTTP server started", {
    port: PORT,
    environment: process.env.NODE_ENV || "development"
  });

  console.log(`Server running at http://localhost:${PORT}`);
  console.log("Try /health, /, and /error to inspect different log scenarios.");
});
Enter fullscreen mode Exit fullscreen mode

Conclusion

Effective logging is an observability feature, not simply a collection of console.log() statements. Good logs answer practical questions: what happened, when did it happen, which request caused it, how long did it take, and what failed?

For production Node.js systems, prefer structured logs, meaningful log levels, correlation IDs, consistent metadata, and explicit redaction of sensitive information. Keep messages useful and intentional instead of logging every variable or every function call.

As applications grow, centralized log collection and monitoring become increasingly valuable. A disciplined logging strategy gives development and operations teams the context they need to troubleshoot incidents quickly while keeping production systems secure and maintainable.

Top comments (0)