How to Handle Configuration Management in JavaScript Applications: Clean Code Principles

Keep configuration separate from business logic by using explicit default objects, centralized config modules, and dependency injection to avoid magic values and hidden flags.

Clean configuration management determines whether a JavaScript codebase remains maintainable as it scales. According to the ryanmcdermott/clean-code-javascript repository, proper configuration handling requires decoupling settings from logic, using immutable central objects, and avoiding boolean switches that obscure intent.

Use Explicit Default Objects with Object.assign

The clean-code-javascript guidelines emphasize defining default configuration objects and merging overrides with Object.assign. This approach prevents scattered || fallbacks throughout your codebase and makes the expected shape of your configuration immediately visible to other developers.

In the repository's README, the section "Set default objects with Object.assign" demonstrates how to create predictable configuration by merging objects rather than relying on inline conditionals.

// src/config.js
const defaults = {
  apiBaseUrl: 'https://api.example.com',
  requestTimeout: 5000,
  enableLogging: false,
};

const envOverrides = {
  apiBaseUrl: process.env.API_BASE_URL,
  requestTimeout: process.env.REQUEST_TIMEOUT && Number(process.env.REQUEST_TIMEOUT),
  enableLogging: process.env.ENABLE_LOGGING === 'true',
};

const config = Object.assign({}, defaults, envOverrides);
Object.freeze(config); // make immutable

export default config;

Centralize Configuration in Dedicated Modules

Hard-coded values sprinkled across functions create technical debt. Instead, place all configurable values—API endpoints, feature toggles, and limits—in a dedicated module or JSON file. Import this central configuration wherever needed, ensuring the rest of your application never contains magic strings or numbers.

This pattern aligns with the repository's principle of making configuration explicit and discoverable, reducing the risk of inconsistent settings between modules.

Avoid Boolean Flags for Configuration

The clean-code-javascript README specifically warns against using boolean flags as function parameters in the section "Don't use flags as function parameters". When a function accepts a boolean to toggle behavior, it signals that the function violates the Single Responsibility Principle by doing more than one thing.

Split responsibilities into separate, clearly-named functions rather than hiding configuration logic inside conditional branches.

// Bad – flag hides two responsibilities
function save(data, encrypt) {
  if (encrypt) {
    data = encryptData(data);
  }
  writeToDisk(data);
}

// Good – separate functions with explicit intent
function savePlain(data) {
  writeToDisk(data);
}
function saveEncrypted(data) {
  writeToDisk(encryptData(data));
}

Inject Dependencies Instead of Importing Globals

Directly importing configuration modules creates tight coupling and complicates testing. Instead, pass configuration objects—or specific slices of them—into functions and classes that need them. This follows the Dependency Inversion Principle outlined in the SOLID section of the repository.

Dependency injection makes components reusable across different contexts and allows tests to provide mock configurations without monkey-patching global imports.

// src/apiClient.js
import fetch from 'node-fetch';

export default class ApiClient {
  constructor({ apiBaseUrl, requestTimeout, enableLogging }) {
    this.baseUrl = apiBaseUrl;
    this.timeout = requestTimeout;
    this.logging = enableLogging;
  }

  async get(resource) {
    const url = `${this.baseUrl}/${resource}`;
    if (this.logging) console.log(`GET ${url}`);
    const response = await fetch(url, { timeout: this.timeout });
    return response.json();
  }
}

Wire everything together at your application entry point to maintain loose coupling:

// src/index.js
import config from './config.js';
import ApiClient from './apiClient.js';

const client = new ApiClient(config);
client.get('users').then(users => console.log(users));

Secure Secrets with Environment Variables

Never commit credentials or environment-specific settings to source control. Load process.env values at the application entry point and map them into your central config object. This keeps secrets out of Git history while allowing deployment-specific tweaks without code changes.

The example in src/config.js demonstrates this pattern by reading API_BASE_URL and ENABLE_LOGGING from environment variables and merging them with sensible defaults.

Immutable Configuration After Initialization

Treat the final merged configuration as read-only to prevent accidental runtime mutations that cause hard-to-track bugs. Use Object.freeze() immediately after constructing your config object, as shown in the src/config.js example. This ensures that once your application starts, its configuration remains stable and predictable.

Summary

  • Use Object.assign to merge default configuration objects with environment-specific overrides, avoiding scattered fallback logic.
  • Centralize all settings in a dedicated config module to eliminate magic values from business logic.
  • Avoid boolean flags as function parameters; split conditional behaviors into separate, explicitly named functions.
  • Inject configuration into classes and functions rather than importing global config modules directly.
  • Freeze configuration objects after initialization to prevent runtime mutations.
  • Load secrets from environment variables at the entry point, keeping sensitive data out of source control.

Frequently Asked Questions

How do I merge default configuration with environment variables in JavaScript?

Create a defaults object containing sensible fallbacks, then use Object.assign({}, defaults, envOverrides) to merge environment-specific values. This pattern, recommended in the clean-code-javascript README, ensures your application works out-of-the-box while allowing customization via process.env variables.

Why should I avoid boolean flags as function parameters in JavaScript?

Boolean flags force functions to handle multiple responsibilities, violating the Single Responsibility Principle. As documented in the clean-code-javascript repository's "Don't use flags as function parameters" section, splitting behaviors into separate functions like savePlain() and saveEncrypted() makes code more explicit and easier to test.

What is the best way to handle secrets in JavaScript applications?

Load secrets exclusively through environment variables at your application entry point, then merge them into a centralized, frozen configuration object. Never hard-code credentials or commit .env files to version control; instead, inject the configuration into modules that require authentication details.

How does dependency injection improve configuration management?

Dependency injection decouples your business logic from configuration sources by passing settings as constructor or function parameters. This approach, aligned with the SOLID principles in ryanmcdermott/clean-code-javascript, enables easier unit testing with mock configs and prevents hidden global state from complicating your application architecture.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →