Rust Application Configuration: config and dotenv Libraries
Any modern application requires a flexible configuration system: database settings, API keys, server ports, logging levels, and other parameters. In the Rust ecosystem, there are powerful tools for this — the config and dotenv crates. They allow you to combine settings from different sources (files, environment variables, command-line arguments) into a single type-safe structure.
Why a separate library?
Rust is a language with strong static typing. Simply reading strings from std::env::var() leads to numerous manual checks and conversions. The config library automates this process:
- Format support: YAML, JSON, TOML, HJSON, INI, RON
- Hierarchical configuration structure with override capability
- Automatic deserialization into custom structures via
serde - Merging multiple sources with priorities (file < env < CLI)
Installation
Add to Cargo.toml:
[dependencies]config = "0.14"serde = { version = "1.0", features = ["derive"] }dotenv = "0.15"Basic example: reading from file and env
Let's create a structure for the application configuration:
use serde::Deserialize;use config::Config;
#[derive(Debug, Deserialize)]struct AppConfig { server: ServerConfig, database: DatabaseConfig,}
#[derive(Debug, Deserialize)]struct ServerConfig { host: String, port: u16, workers: u8,}
#[derive(Debug, Deserialize)]struct DatabaseConfig { url: String, pool_size: u32,}
fn main() -> Result<(), config::ConfigError> { // Load configuration let config = Config::builder() .add_source(config::File::with_name("config/default")) .add_source(config::Environment::with_prefix("APP")) .build()?;
let app_config: AppConfig = config.try_deserialize()?; println!("{:#?}", app_config); Ok(())}File config/default.toml:
[server]host = "127.0.0.1"port = 8080workers = 4
[database]url = "postgres://localhost/mydb"pool_size = 10Environment variables with the APP_ prefix override values from the file. For example, APP_DATABASE__URL will change the database URL.
Working with dotenv
For development convenience, a .env file is often used. The dotenv crate loads it into environment variables before initializing config:
use dotenv::dotenv;
fn main() { dotenv().ok(); // load .env // then initialize config as in the example above}Contents of .env:
APP_SERVER__PORT=3000APP_DATABASE__POOL_SIZE=20Advanced scenario: multiple sources
Real projects require combining settings from different places. config allows you to specify a chain of sources with priority:
let config = Config::builder() // Base settings (low priority) .add_source(config::File::with_name("config/default")) // Override for a specific environment .add_source(config::File::with_name(&format!("config/{}", env::var("APP_ENV").unwrap_or("development")))) // Environment variables (medium priority) .add_source(config::Environment::with_prefix("APP")) // Command-line arguments (high priority) .add_source(config::File::from(std::env::args().nth(1).unwrap_or_default())) .build()?;Best Practices
- Use prefixes for environment variables to avoid conflicts with system variables