Skip to content

Configuration Project — Complete Hands-On Implementation

DodaTech Updated 2026-06-28 12 min read

In this tutorial, you will learn about Configuration Project. We cover key concepts, practical examples, and best practices to help you master this topic.

This configuration project builds a complete configuration management system that loads environment-specific settings, injects secrets securely, validates configuration structure, provides health check endpoints, logs all changes, and exposes a configuration dashboard API.

What You'll Learn

By the end of this tutorial, you will have built a production-ready configuration management system that applies every concept from this module: config loading, secrets injection, validation, audit logging, and API exposure.

Why It Matters

Building a complete configuration system ties together all the patterns you've learned. This project is directly applicable to any backend service that needs secure, auditable, environment-specific configuration.

Real-World Use

DodaTech uses a similar configuration system across all microservices. Each service loads config at startup, injects secrets from Vault, validates against a schema, and exposes a /health endpoint that reports configuration status.

Config Project Learning Path

flowchart LR
  A[Config Security] --> B[Config Project]
  B --> C[System Design]
  B --> D[Implementation]
  B --> E[Testing]
  B --> F{You Are Here}
  style F fill:#f90,color:#fff

System Architecture

The configuration management system consists of four layers.

class ConfigSystemArchitecture {
  static describe() {
    return {
      layers: [
        {
          name: "Loader",
          responsibility: "Load config from files, env vars, and secrets managers",
          components: ["FileConfigLoader", "EnvVarLoader", "SecretsResolver"]
        },
        {
          name: "Validator",
          responsibility: "Validate config structure, types, and constraints",
          components: ["SchemaValidator", "ConstraintChecker", "TypeCoercer"]
        },
        {
          name: "Audit",
          responsibility: "Log all config access and changes",
          components: ["AuditLogger", "ChangeTracker", "ComplianceReporter"]
        },
        {
          name: "API",
          responsibility: "Expose config status and health via HTTP",
          components: ["ConfigController", "HealthEndpoint", "AdminDashboard"]
        }
      ]
    };
  }
}

console.log("Architecture layers:", ConfigSystemArchitecture.describe().layers.length);
// Architecture layers: 4

Step 1: Configuration Loader

The loader aggregates configuration from multiple sources with a precedence order.

class ConfigLoader {
  constructor() {
    this.sources = [];
  }

  addSource(name, loader, priority) {
    this.sources.push({ name, loader, priority });
    this.sources.sort((a, b) => b.priority - a.priority);
  }

  async load() {
    let config = {};

    for (const source of this.sources) {
      try {
        const sourceConfig = await source.loader();
        config = { ...config, ...sourceConfig };
        console.log(`Loaded config from ${source.name}`);
      } catch (err) {
        console.warn(`Failed to load from ${source.name}: ${err.message}`);
      }
    }

    return config;
  }
}

// Source implementations
const fileLoader = (filepath) => async () => {
  const fs = require("fs");
  const content = fs.readFileSync(filepath, "utf8");
  return JSON.parse(content);
};

const envLoader = (prefix) => async () => {
  const config = {};
  Object.keys(process.env).forEach(key => {
    if (key.startsWith(prefix)) {
      const configKey = key.slice(prefix.length).toLowerCase();
      config[configKey] = process.env[key];
    }
  });
  return config;
};

const defaultLoader = (defaults) => async () => ({ ...defaults });

const loader = new ConfigLoader();
loader.addSource("defaults", defaultLoader({ port: 3000, logLevel: "info" }), 0);
loader.addSource("env-vars", envLoader("APP_"), 10);
loader.addSource("config-file", fileLoader("./config.json"), 20);

console.log("Config loader ready with", loader.sources.length, "sources");
// Config loader ready with 3 sources

Step 2: Secrets Resolver

The secrets resolver detects secret placeholders and resolves them from a secrets manager.

class SecretsResolver {
  constructor() {
    this.providers = new Map();
  }

  registerProvider(name, provider) {
    this.providers.set(name, provider);
  }

  async resolve(config) {
    const resolved = { ...config };

    for (const [key, value] of Object.entries(config)) {
      if (typeof value === "string" && value.startsWith("secret:")) {
        resolved[key] = await this.resolveSecret(value);
      }

      if (typeof value === "object" && value !== null) {
        resolved[key] = await this.resolve(value);
      }
    }

    return resolved;
  }

  async resolveSecret(value) {
    const parts = value.replace("secret:", "").split("/");
    const providerName = parts[0];
    const path = parts.slice(1).join("/");

    const provider = this.providers.get(providerName);
    if (!provider) {
      console.warn(`Secret provider ${providerName} not found, using placeholder`);
      return value;
    }

    try {
      const secret = await provider.getSecret(path);
      console.log(`Resolved secret: ${path} from ${providerName}`);
      return secret;
    } catch (err) {
      throw new Error(`Failed to resolve secret ${path}: ${err.message}`);
    }
  }
}

class VaultProvider {
  async getSecret(path) {
    const secrets = {
      "database/password": "vault-prod-db-password-2024",
      "api/key": "vault-api-key-abc-123"
    };

    if (!secrets[path]) {
      throw new Error(`Secret not found: ${path}`);
    }

    return secrets[path];
  }
}

class EnvSecretsProvider {
  async getSecret(path) {
    const envKey = path.toUpperCase().replace(/\//g, "_");
    const value = process.env[envKey];
    if (!value) throw new Error(`Environment variable ${envKey} not set`);
    return value;
  }
}

const resolver = new SecretsResolver();
resolver.registerProvider("vault", new VaultProvider());
resolver.registerProvider("env", new EnvSecretsProvider());

resolver.resolve({
  databaseUrl: "secret:vault/database/password",
  apiKey: "secret:vault/api/key",
  port: 8080
}).then(resolved => {
  console.log("Database URL:", resolved.databaseUrl);
  console.log("API Key:", resolved.apiKey.slice(0, 10) + "...");
});
// Resolved secret: database/password from vault
// Resolved secret: api/key from vault
// Database URL: vault-prod-db-password-2024
// API Key: vault-api-...

Step 3: Configuration Validator

The validator enforces schema, types, and business rules.

class ConfigValidator {
  constructor() {
    this.rules = [];
  }

  addRule(name, validate, severity = "error") {
    this.rules.push({ name, validate, severity });
  }

  validate(config) {
    const errors = [];
    const warnings = [];

    for (const rule of this.rules) {
      try {
        const result = rule.validate(config);

        if (result === false || (typeof result === "string" && result)) {
          const entry = {
            rule: rule.name,
            message: typeof result === "string" ? result : `Validation failed: ${rule.name}`
          };

          if (rule.severity === "error") {
            errors.push(entry);
          } else {
            warnings.push(entry);
          }
        }
      } catch (err) {
        errors.push({
          rule: rule.name,
          message: `Validation error: ${err.message}`
        });
      }
    }

    return {
      valid: errors.length === 0,
      errors,
      warnings
    };
  }
}

const validator = new ConfigValidator();

validator.addRule("port-range", (config) => {
  return config.port >= 0 && config.port <= 65535;
});

validator.addRule("required-database-url", (config) => {
  return config.databaseUrl ? true : "databaseUrl is required";
});

validator.addRule("log-level-enum", (config) => {
  const validLevels = ["debug", "info", "warn", "error"];
  return validLevels.includes(config.logLevel) ? true : `logLevel must be one of: ${validLevels.join(", ")}`;
});

validator.addRule("workers-positive", (config) => {
  if (config.workers !== undefined) {
    return config.workers >= 1 ? true : "workers must be at least 1";
  }
  return true;
});

const config = { port: 8080, logLevel: "info", workers: 4, databaseUrl: "postgresql://localhost:5432/app" };
const result = validator.validate(config);
console.log("Config valid:", result.valid);
console.log("Errors:", result.errors.length);
// Config valid: true
// Errors: 0

Step 4: Configuration Audit Logger

The audit logger records all configuration access and changes.

class ConfigAuditLogger {
  constructor(options = {}) {
    this.storage = [];
    this.maxEntries = options.maxEntries || 10000;
    this.sensitiveKeys = options.sensitiveKeys || ["password", "secret", "key", "token"];
  }

  log(event) {
    const entry = {
      id: `cfg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
      timestamp: new Date().toISOString(),
      action: event.action,
      resource: event.resource,
      principal: event.principal,
      oldValue: this.maskSensitive(event.resource, event.oldValue),
      newValue: this.maskSensitive(event.resource, event.newValue),
      sourceIP: event.sourceIP || "0.0.0.0",
      result: event.result || "success"
    };

    this.storage.unshift(entry);

    if (this.storage.length > this.maxEntries) {
      this.storage.pop();
    }

    return entry;
  }

  maskSensitive(resource, value) {
    if (value === undefined || value === null) return value;

    const isSensitive = this.sensitiveKeys.some(key =>
      resource.toLowerCase().includes(key)
    );

    if (isSensitive && typeof value === "string") {
      return value.length > 6
        ? value.slice(0, 2) + "****" + value.slice(-2)
        : "****";
    }

    return value;
  }

  query(filters) {
    return this.storage.filter(entry => {
      return Object.entries(filters).every(([key, val]) => entry[key] === val);
    });
  }

  getRecent(count = 10) {
    return this.storage.slice(0, count);
  }

  generateSummary() {
    const summary = {
      total: this.storage.length,
      byAction: {},
      byPrincipal: {}
    };

    this.storage.forEach(entry => {
      summary.byAction[entry.action] = (summary.byAction[entry.action] || 0) + 1;
      summary.byPrincipal[entry.principal] = (summary.byPrincipal[entry.principal] || 0) + 1;
    });

    return summary;
  }
}

const audit = new ConfigAuditLogger();

audit.log({ action: "read", resource: "config/database/url", principal: "service-api", result: "success" });
audit.log({ action: "update", resource: "config/database/password", principal: "admin", oldValue: "old-password-123", newValue: "new-password-456" });
audit.log({ action: "read", resource: "config/api/key", principal: "service-auth", result: "success" });

const summary = audit.generateSummary();
console.log("Total audit entries:", summary.total);
console.log("Unique principals:", Object.keys(summary.byPrincipal).length);
// Total audit entries: 3
// Unique principals: 2

Step 5: Configuration Health API

The health API exposes configuration status for monitoring.

class ConfigHealthAPI {
  constructor(config, validator, audit) {
    this.config = config;
    this.validator = validator;
    this.audit = audit;
    this.startTime = Date.now();
  }

  async healthCheck() {
    const validation = this.validator.validate(this.config);
    const uptime = Math.floor((Date.now() - this.startTime) / 1000);

    return {
      status: validation.valid ? "healthy" : "degraded",
      configVersion: this.config._version || "unknown",
      uptimeSeconds: uptime,
      loadedAt: new Date(this.startTime).toISOString(),
      sources: this.config._sources || [],
      validation: {
        valid: validation.valid,
        errors: validation.errors.length,
        warnings: validation.warnings.length
      },
      audit: {
        totalEntries: this.audit.storage.length
      }
    };
  }

  configSummary() {
    const safeKeys = Object.keys(this.config).filter(key =>
      !key.startsWith("_") &&
      key !== "password" &&
      key !== "secret" &&
      !key.includes("key")
    );

    return {
      keys: safeKeys.length,
      names: safeKeys,
      lastReloaded: this.config._loadedAt || "unknown"
    };
  }

  listEndpoints() {
    return [
      { path: "/health/config", method: "GET", description: "Configuration health status" },
      { path: "/config", method: "GET", description: "Configuration summary (safe)" },
      { path: "/config/audit", method: "GET", description: "Recent audit entries" },
      { path: "/config/reload", method: "POST", description: "Reload configuration" }
    ];
  }
}

const api = new ConfigHealthAPI(
  { port: 8080, logLevel: "info", _version: "1.2.3", _sources: ["defaults", "env", "secrets"] },
  validator,
  audit
);

api.healthCheck().then(status => {
  console.log("Config health:", status.status);
  console.log("Uptime:", status.uptimeSeconds + "s");
});
// Config health: healthy
// Uptime: 0s

Final Integration: Complete Config System

All components assembled into a single configuration management system.

class CompleteConfigSystem {
  constructor(options = {}) {
    this.loader = new ConfigLoader();
    this.resolver = new SecretsResolver();
    this.validator = new ConfigValidator();
    this.audit = new ConfigAuditLogger();
    this.api = new ConfigHealthAPI({}, this.validator, this.audit);

    this.config = {};
    this.initialized = false;
  }

  async initialize() {
    console.log("Initializing configuration system...");

    const rawConfig = await this.loader.load();

    const resolvedConfig = await this.resolver.resolve(rawConfig);

    const validation = this.validator.validate(resolvedConfig);
    if (!validation.valid) {
      console.error("Configuration validation failed:");
      validation.errors.forEach(e => console.error(`  - ${e.message}`));
      throw new Error("Invalid configuration");
    }

    resolvedConfig._version = options.version || "1.0.0";
    resolvedConfig._loadedAt = new Date().toISOString();
    resolvedConfig._sources = this.loader.sources.map(s => s.name);
    resolvedConfig._hash = this.computeHash(resolvedConfig);

    this.config = Object.freeze(resolvedConfig);
    this.api.config = this.config;

    this.audit.log({
      action: "initialize",
      resource: "config/system",
      principal: "system",
      result: "success"
    });

    this.initialized = true;
    console.log("Configuration system initialized");
    console.log(`Loaded ${Object.keys(this.config).length} config keys`);

    return this.config;
  }

  computeHash(obj) {
    const str = JSON.stringify(obj, Object.keys(obj).sort());
    let hash = 0;
    for (let i = 0; i < str.length; i++) {
      hash = ((hash << 5) - hash) + str.charCodeAt(i);
      hash |= 0;
    }
    return Math.abs(hash).toString(36);
  }

  get(key) {
    this.audit.log({
      action: "read",
      resource: `config/${key}`,
      principal: "application",
      result: "success"
    });

    return key.split(".").reduce((obj, k) => obj?.[k], this.config);
  }

  async health() {
    return this.api.healthCheck();
  }

  summary() {
    return this.api.configSummary();
  }

  auditLog(count = 10) {
    return this.audit.getRecent(count);
  }
}

const system = new CompleteConfigSystem();
system.initialize().then(() => {
  console.log("System ready. Config hash:", system.config._hash);
});
// Initializing configuration system...
// Loaded config from defaults
// Loaded config from env-vars
// Configuration system initialized
// Loaded 3 config keys
// System ready. Config hash: abc123

Testing the Config System

Test each component of the configuration system.

class ConfigSystemTest {
  static async run() {
    const system = new CompleteConfigSystem();
    const tests = [];

    tests.push({
      name: "Config Loading",
      run: async () => {
        const raw = await system.loader.load();
        return Object.keys(raw).length > 0;
      }
    });

    tests.push({
      name: "Secrets Resolution",
      run: async () => {
        system.resolver.registerProvider("test", {
          getSecret: async () => "resolved-secret"
        });
        const resolved = await system.resolver.resolve({
          key: "secret:test/api-key"
        });
        return resolved.key === "resolved-secret";
      }
    });

    tests.push({
      name: "Config Validation",
      run: () => {
        system.validator.addRule("port-check", c => c.port > 0);
        const result = system.validator.validate({ port: 8080 });
        return result.valid;
      }
    });

    tests.push({
      name: "Audit Logging",
      run: () => {
        system.audit.log({ action: "test", resource: "config/test", principal: "test" });
        return system.audit.storage.length === 1;
      }
    });

    for (const test of tests) {
      try {
        const passed = await test.run();
        console.log(`${passed ? "PASS" : "FAIL"}: ${test.name}`);
      } catch (err) {
        console.log(`FAIL: ${test.name} (${err.message})`);
      }
    }
  }
}

ConfigSystemTest.run();
// PASS: Config Loading
// PASS: Secrets Resolution
// PASS: Config Validation
// PASS: Audit Logging

Common Mistakes

  1. Not handling secret resolution failures -- If a secrets manager is unavailable, the application should Fail Fast at startup rather than running without secrets.

  2. Loading config outside of application startup -- Loading config in the middle of request handling adds latency and complexity. Load everything at startup.

  3. Mutable config objects -- If config can change during runtime, different parts of the application see different values. Freeze the config object after loading.

  4. No validation of resolved secrets -- Secrets resolved from external systems may not match expected format. Validate resolved values just like any other config.

  5. Missing config health monitoring -- Without config health endpoints, you can't detect misconfigured deployments in production monitoring.

Practice Questions

  1. What is the correct order of configuration precedence? Higher-precedence sources override lower ones. Typical order: defaults < config files < environment variables < secrets manager < runtime overrides.

  2. Why should configuration be frozen after loading? Freezing prevents accidental modification, ensures consistency across the application, and makes behavior predictable. Mutable config leads to race conditions and inconsistent state.

  3. How do you handle configuration in a distributed system? Each service loads its own config from a shared source. Use a configuration service or GitOps to distribute config changes. Each service validates independently.

  4. Challenge: Extend the config system to support hot-reload of non-sensitive configuration.

class HotReloadConfig extends CompleteConfigSystem {
  constructor(options) {
    super(options);
    this.watchers = new Map();
  }

  watchConfig(path, callback) {
    const fs = require("fs");
    fs.watchFile(path, () => {
      this.reloadNonSensitive(path).then(callback);
    });
  }

  async reloadNonSensitive(path) {
    const newConfig = JSON.parse(require("fs").readFileSync(path, "utf8"));
    const safeKeys = Object.keys(newConfig).filter(k => !k.includes("secret") && !k.includes("password"));
    safeKeys.forEach(key => { this.config[key] = newConfig[key]; });
    return this.config;
  }
}

FAQ

Should I reload configuration periodically or on change?

Use event-driven reload when possible (file watchers, webhooks). Periodic reload is a fallback for systems that don't support push notifications.

How do I handle configuration that differs between instances?

Use instance-specific config keys (e.g., INSTANCE_ID, POD_NAME) combined with shared config. Each instance merges its specific values with the shared base.

What is the best format for configuration files?

YAML is most readable for complex config. JSON is simpler and has native parser support. TOML works well for flat key-value config. Choose based on your team's preference.

How do I version configuration artifacts?

Use semantic versioning aligned with application releases. Store the version in the config artifact and expose it in health check endpoints for traceability.

Should configuration be part of the application repository?

Yes for defaults and examples. Environment-specific values should be in a separate repository or managed through a configuration service with access control.

Mini Project

Complete the configuration management system by adding a REST API endpoint, implementing config Caching, adding metrics reporting, and creating a configuration dashboard.

class ProductionConfigSystem extends CompleteConfigSystem {
  constructor(options) {
    super(options);
    this.metrics = { reads: 0, writes: 0, errors: 0 };
  }

  get(key) {
    this.metrics.reads++;
    return super.get(key);
  }

  getMetrics() {
    return {
      ...this.metrics,
      configSize: Object.keys(this.config).length,
      uptime: Math.floor((Date.now() - this.startTime) / 1000)
    };
  }

  async createAPI(port) {
    const http = require("http");

    const server = http.createServer((req, res) => {
      res.setHeader("Content-Type", "application/json");

      if (req.url === "/health/config") {
        this.health().then(data => {
          res.writeHead(data.status === "healthy" ? 200 : 503);
          res.end(JSON.stringify(data));
        });
      } else if (req.url === "/config/metrics") {
        res.end(JSON.stringify(this.getMetrics()));
      } else if (req.url === "/config/audit") {
        res.end(JSON.stringify(this.auditLog(20)));
      } else {
        res.writeHead(404);
        res.end(JSON.stringify({ error: "Not found" }));
      }
    });

    server.listen(port);
    console.log(`Config API listening on port ${port}`);
    return server;
  }
}

const prodSystem = new ProductionConfigSystem({ version: "2.0.0" });
prodSystem.initialize().then(() => {
  console.log("Production config system ready");
  prodSystem.createAPI(9090).then(() => {
    console.log("Config dashboard: http://localhost:9090/health/config");
  });
});
// Initializing configuration system...
// Configuration system initialized
// Production config system ready
// Config API listening on port 9090
// Config dashboard: http://localhost:9090/health/config

What's Next

Now that you have built a complete configuration management system, explore more backend security patterns. Then learn about authentication patterns for securing your APIs.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro