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.