DevPath · Learn to code ESPTEN

Production and deployment

Configuration: 12-factor and environments

The configuration problem

The same server runs in several environments: your laptop (development), a test server (staging), and production (production). Each one needs different values: the port, the database URL, the keys for external APIs... If you hardcode those values in the code, you mix what changes (the config) with what doesn't (the program), and you end up with an if (environment === "prod") scattered everywhere.

The 12-factor methodology

The Twelve-Factor App is a set of best practices for building services. Its factor III (Config) says:

Store configuration in the environment, not in the code.

Specifically:

// In Node, environment variables arrive in process.env (always strings).
const PORT = process.env.PORT;          // "3000"
const NODE_ENV = process.env.NODE_ENV;  // "production"

Centralize and set default values

Instead of reading process.env all over the code, centralize the reading in a single config module that validates and applies default values. That way the rest of the application receives a clean, typed object:

function loadConfig(env) {
  return {
    port: Number(env.PORT) || 3000,
    environment: env.NODE_ENV || "development",
  };
}

const config = loadConfig(process.env);

In these exercises we inject the environment as an env object (instead of reading process.env directly). This makes the config testable: you can test the same function with {}, with { PORT: "8080" }, etc.

Separate config per environment

The value of NODE_ENV decides the behavior: in development you want detailed logs and full error messages; in production, compact logs and generic errors (so as not to leak internal details). The code is the same; only the values it receives change.

Secret management

Passwords, tokens, and keys are especially sensitive config:

Examples

Centralized config with defaults and secret validation

function loadConfig(env) {
  if (env.NODE_ENV === "production" && !env.DATABASE_URL) {
    throw new Error("DATABASE_URL is missing in production");
  }
  return {
    port: Number(env.PORT) || 3000,
    environment: env.NODE_ENV || "development",
    dbUrl: env.DATABASE_URL || "memory://local",
  };
}

console.log(loadConfig({ PORT: "8080" }));
console.log(loadConfig({}));
Put this into practice

DevPath is a hands-on course: you read the theory here; in the app you put it into practice with exercises that really run, offline.

Start free in the app →
Logging and observability →