Phase 2 · Web Servers & HTTP APIsModule 14~60 min read

Configuration, Errors & Observability

Turn failures into safe, consistent API responses while validating configuration, producing useful logs, and shutting down cleanly.

What you'll learn

Production APIs fail, restart, and receive bad input. You will validate startup configuration, translate expected errors, emit safe structured logs, expose health signals, and close the server without dropping control of the lifecycle.

By the end of this lesson, you'll be able to:

  • Validate environment configuration at startup
  • Model operational HTTP errors without leaking internals
  • Create Express 5 not-found and error middleware
  • Add structured logs, health checks, and graceful shutdown

Core mental model

Node.js becomes easier when you separate the JavaScript language from the runtime and the operating-system capabilities it exposes. Use this table as a decision guide.

ConceptWhat it meansDecision rule
Configuration boundaryEnvironment strings become validated application settingsFail startup before listening when required configuration is invalid
Operational errorAn expected failure such as invalid input or missing dataTranslate it into a safe status, code, and message
Programmer errorA defect or broken invariantReturn 500, log full diagnostics privately, and fix the code
ObservabilityLogs and health signals explain system behaviorRecord structured context without secrets or entire payloads

Professional workflow

Build and verify Node.js programs from the terminal in small, observable steps.

  1. Parse and validate configuration before app construction
  2. Assign stable error codes to expected failures
  3. Let async handler rejections reach Express error middleware
  4. Log one diagnostic event with request context
  5. Send a safe consistent error representation
  6. Expose liveness and dependency-aware readiness separately
  7. Stop accepting traffic and close dependencies on signals

Keep the feedback loop short

Run the smallest useful command after every meaningful change. Read the complete error message before editing again, and keep inputs and outputs visible while you learn.

Guided code lab

Validate configuration once

Environment values are strings. The loader converts, validates, freezes, and returns the settings the rest of the app can trust.

config.js
export function loadConfig(environment = process.env) {
  const port = Number(environment.PORT ?? 3000);
  if (!Number.isInteger(port) || port < 0 || port > 65535) {
    throw new Error("PORT must be an integer from 0 to 65535");
  }
  const nodeEnv = environment.NODE_ENV ?? "development";
  if (!["development", "test", "production"].includes(nodeEnv)) {
    throw new Error("NODE_ENV is invalid");
  }
  return Object.freeze({ port, nodeEnv });
}

Define and translate expected errors

The public code is stable for clients while logs retain stack and request context. Unexpected errors receive a generic message.

errors.js
export class HttpError extends Error {
  constructor(status, code, message, options = {}) {
    super(message, options);
    this.status = status;
    this.code = code;
  }
}

export function errorHandler(error, request, response, next) {
  if (response.headersSent) return next(error);

  const expected = error instanceof HttpError;
  const status = expected ? error.status : 500;
  console.error(JSON.stringify({
    requestId: request.context?.requestId,
    message: error.message,
    stack: error.stack,
  }));
  response.status(status).json({
    error: {
      code: expected ? error.code : "INTERNAL_ERROR",
      message: expected ? error.message : "An unexpected error occurred",
    },
  });
}

Coordinate graceful shutdown

The process stops accepting new requests, waits for server close, then releases dependencies. A timeout prevents shutdown from hanging forever.

shutdown.js
export function registerShutdown(server, closeDependencies) {
  async function shutdown(signal) {
    console.log(JSON.stringify({ event: "shutdown_started", signal }));
    const forceTimer = setTimeout(() => process.exit(1), 10_000);
    forceTimer.unref();

    server.close(async (error) => {
      try {
        await closeDependencies();
        if (error) process.exitCode = 1;
      } catch (cause) {
        console.error(cause);
        process.exitCode = 1;
      } finally {
        clearTimeout(forceTimer);
      }
    });
  }
  process.once("SIGINT", shutdown);
  process.once("SIGTERM", shutdown);
}

Production practice

Fail before listen

Invalid required settings should stop startup, not surface later during the first real request.

Log privately

Logs may contain stack traces and safe identifiers; responses must not expose filesystem paths, SQL, tokens, or internal stacks.

Separate health

Liveness says the process is alive; readiness says it can currently serve traffic using required dependencies.

Common failure mode

Catching every error and returning its message to clients leaks internals and treats programmer defects as ordinary input. Classify expected errors and make unknown failures generic externally.

Independent workshop

Harden the project API with validated startup, structured errors, safe logs, health endpoints, and shutdown behavior.

Your finished workshop must include:

  • A frozen validated config object
  • Stable public error codes
  • 404 and four-argument error middleware last
  • Request-correlated structured logs
  • Separate live and ready endpoints
  • Shutdown timeout and dependency cleanup

Definition of done

Run the happy path and at least two edge cases, keep responsibilities separated, and add a short README explaining how to run the program.

Recap & quick check

Key takeaways

  • Configuration is untrusted input
  • Expected errors have stable public contracts
  • Unexpected errors are logged but hidden from clients
  • Express error middleware has four parameters
  • Observability needs request context
  • Graceful shutdown owns server and dependency lifecycle

Quick check

1. When should invalid required configuration fail?

2. How many parameters identify Express error middleware?

3. What should an unexpected error response expose?

4. What does readiness communicate?

Next: API Testing & Documentation