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.
| Concept | What it means | Decision rule |
|---|---|---|
| Configuration boundary | Environment strings become validated application settings | Fail startup before listening when required configuration is invalid |
| Operational error | An expected failure such as invalid input or missing data | Translate it into a safe status, code, and message |
| Programmer error | A defect or broken invariant | Return 500, log full diagnostics privately, and fix the code |
| Observability | Logs and health signals explain system behavior | Record structured context without secrets or entire payloads |
Professional workflow
Build and verify Node.js programs from the terminal in small, observable steps.
- Parse and validate configuration before app construction
- Assign stable error codes to expected failures
- Let async handler rejections reach Express error middleware
- Log one diagnostic event with request context
- Send a safe consistent error representation
- Expose liveness and dependency-aware readiness separately
- Stop accepting traffic and close dependencies on signals
Keep the feedback loop short
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.
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.
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.
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
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
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