How to Use the config Crate for Application Configuration

Use the `config` crate to build a flexible, layered configuration system that merges defaults, environment variables, and file-based settings (like YAML, JSON, or TOML) into a single structured Rust type.

Use the config crate to build a flexible, layered configuration system that merges defaults, environment variables, and file-based settings (like YAML, JSON, or TOML) into a single structured Rust type. You define a Config builder, add sources in priority order (where later sources override earlier ones), and then deserialize the final result into your application's configuration struct.

Here is a practical example using serde to define a config struct and the config crate to load it from a YAML file, environment variables, and default values:

use config::{Config, ConfigError, File};
use serde::Deserialize;
use std::path::Path;

#[derive(Debug, Deserialize)]
struct AppSettings {
    app_name: String,
    port: u16,
    debug: bool,
    database: DatabaseConfig,
}

#[derive(Debug, Deserialize)]
struct DatabaseConfig {
    host: String,
    port: u16,
}

fn load_config() -> Result<AppSettings, ConfigError> {
    let settings = Config::builder()
        // 1. Set defaults (lowest priority)
        .set_default("app_name", "MyApp")?
        .set_default("port", 8080)?
        .set_default("debug", false)?
        .set_default("database.host", "localhost")?
        .set_default("database.port", 5432)?
        
        // 2. Load from file (overrides defaults)
        .add_source(File::with_name("config").required(false))
        
        // 3. Load from environment variables (highest priority)
        // Maps ENV_VAR_NAME to config key (e.g., APP_NAME -> app_name)
        .add_source(config::Environment::with_prefix("APP_")
            .separator("__")
            .try_parsing(true))
        
        .build()?;

    settings.try_deserialize()
}

fn main() {
    match load_config() {
        Ok(cfg) => println!("Config loaded: {:?}", cfg),
        Err(e) => eprintln!("Configuration error: {}", e),
    }
}

To run this, create a config.yaml file in your project root:

app_name: "ProductionApp"
debug: true
database:
  host: "db.example.com"

Then set an environment variable to override a specific value:

export APP__DATABASE__HOST="override-db.example.com"
cargo run

The crate automatically handles the merging logic. In this setup, the debug flag comes from the file, app_name comes from the file, but the database host is overridden by the environment variable APP__DATABASE__HOST. The try_parsing(true) option ensures that environment variables like APP_PORT=9000 are correctly parsed as integers rather than strings.

For more complex setups, you can add multiple file sources (e.g., config.yaml then config.local.yaml) to support environment-specific overrides without touching the main config file. Always ensure your struct derives Deserialize and matches the nested structure of your configuration sources to avoid runtime errors.