Introdução
A crate Config é a solução mais popular para gerenciamento de configuração em Rust. Ela permite criar sistemas de configuração em camadas, onde valores podem vir de múltiplas fontes – valores padrão, arquivos de configuração, variáveis de ambiente e argumentos de linha de comando – e são mesclados em uma estrutura unificada.
Esse padrão de “layered configuration” é essencial para aplicações modernas que precisam se comportar de forma diferente em cada ambiente (desenvolvimento, staging, produção) sem alterar o código. A crate Config resolve esse problema de forma elegante, integrando-se perfeitamente com o Serde para deserialização type-safe.
Por que usar a crate Config?
- Múltiplas fontes: arquivos, variáveis de ambiente, valores padrão
- Múltiplos formatos: TOML, YAML, JSON, INI, RON
- Merging inteligente: fontes posteriores sobrescrevem anteriores
- Type-safe: deserialização para structs Rust via Serde
- Variáveis de ambiente: mapeamento automático de env vars
- Hierárquico: suporte a configuração aninhada
- Extensível: crie suas próprias fontes de configuração
Instalação
Adicione a crate Config ao seu Cargo.toml:
[dependencies]
config = "0.14"
serde = { version = "1", features = ["derive"] }
Para formatos de arquivo específicos, adicione as features correspondentes:
[dependencies]
config = { version = "0.14", features = [
"toml", # Suporte a TOML
"yaml", # Suporte a YAML
"json", # Suporte a JSON
"ini", # Suporte a INI
"ron", # Suporte a RON
] }
serde = { version = "1", features = ["derive"] }
Uso Básico
Configuração simples com arquivo TOML
Primeiro, crie um arquivo de configuração config/default.toml:
[servidor]
host = "127.0.0.1"
porta = 3000
workers = 4
[banco]
url = "postgres://localhost/app_dev"
max_conexoes = 5
timeout_segundos = 30
[log]
nivel = "info"
formato = "texto"
Agora, carregue e deserialize em uma struct Rust:
use config::{Config, File};
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct Configuracao {
servidor: ConfigServidor,
banco: ConfigBanco,
log: ConfigLog,
}
#[derive(Debug, Deserialize)]
struct ConfigServidor {
host: String,
porta: u16,
workers: usize,
}
#[derive(Debug, Deserialize)]
struct ConfigBanco {
url: String,
max_conexoes: u32,
timeout_segundos: u64,
}
#[derive(Debug, Deserialize)]
struct ConfigLog {
nivel: String,
formato: String,
}
fn main() -> Result<(), config::ConfigError> {
let config = Config::builder()
.add_source(File::with_name("config/default"))
.build()?;
let configuracao: Configuracao = config.try_deserialize()?;
println!("Servidor: {}:{}", configuracao.servidor.host, configuracao.servidor.porta);
println!("Banco: {}", configuracao.banco.url);
println!("Log: {}", configuracao.log.nivel);
Ok(())
}
Configuração com variáveis de ambiente
use config::{Config, Environment, File};
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct Configuracao {
servidor: ConfigServidor,
banco: ConfigBanco,
}
#[derive(Debug, Deserialize)]
struct ConfigServidor {
host: String,
porta: u16,
}
#[derive(Debug, Deserialize)]
struct ConfigBanco {
url: String,
max_conexoes: u32,
}
fn main() -> Result<(), config::ConfigError> {
let config = Config::builder()
// 1. Valores do arquivo de configuração
.add_source(File::with_name("config/default"))
// 2. Variáveis de ambiente com prefixo APP_
// APP_SERVIDOR__HOST -> servidor.host
// APP_BANCO__URL -> banco.url
.add_source(
Environment::with_prefix("APP")
.separator("__") // Use __ para separar níveis
)
.build()?;
let configuracao: Configuracao = config.try_deserialize()?;
println!("{:?}", configuracao);
Ok(())
}
// Uso:
// APP_SERVIDOR__PORTA=8080 APP_BANCO__URL=postgres://prod/app cargo run
Configuração em camadas por ambiente
A abordagem mais comum: ter um arquivo base e sobrescrever por ambiente:
use config::{Config, Environment, File};
use serde::Deserialize;
use std::env;
#[derive(Debug, Deserialize)]
struct Configuracao {
ambiente: String,
servidor: ConfigServidor,
banco: ConfigBanco,
log: ConfigLog,
redis: Option<ConfigRedis>,
}
#[derive(Debug, Deserialize)]
struct ConfigServidor {
host: String,
porta: u16,
workers: usize,
}
#[derive(Debug, Deserialize)]
struct ConfigBanco {
url: String,
max_conexoes: u32,
}
#[derive(Debug, Deserialize)]
struct ConfigLog {
nivel: String,
formato: String,
}
#[derive(Debug, Deserialize)]
struct ConfigRedis {
url: String,
pool_size: u32,
}
fn carregar_configuracao() -> Result<Configuracao, config::ConfigError> {
// Determinar o ambiente (dev, staging, producao)
let ambiente = env::var("APP_ENV").unwrap_or_else(|_| "dev".to_string());
let config = Config::builder()
// Camada 1: Valores padrão
.set_default("ambiente", ambiente.clone())?
.set_default("servidor.host", "127.0.0.1")?
.set_default("servidor.porta", 3000)?
.set_default("servidor.workers", 4)?
.set_default("log.nivel", "debug")?
.set_default("log.formato", "texto")?
// Camada 2: Arquivo de configuração base
.add_source(File::with_name("config/default").required(false))
// Camada 3: Arquivo específico do ambiente
// config/dev.toml, config/staging.toml, config/producao.toml
.add_source(
File::with_name(&format!("config/{}", ambiente)).required(false),
)
// Camada 4: Arquivo local (não commitado no git)
.add_source(File::with_name("config/local").required(false))
// Camada 5: Variáveis de ambiente (maior prioridade)
.add_source(
Environment::with_prefix("APP")
.separator("__")
.try_parsing(true), // Tentar converter "true"/"3000" automaticamente
)
.build()?;
config.try_deserialize()
}
fn main() {
match carregar_configuracao() {
Ok(config) => {
println!("Ambiente: {}", config.ambiente);
println!("Servidor: {}:{}", config.servidor.host, config.servidor.porta);
println!("Workers: {}", config.servidor.workers);
println!("Banco: {}", config.banco.url);
println!("Log: {} ({})", config.log.nivel, config.log.formato);
if let Some(redis) = &config.redis {
println!("Redis: {} (pool: {})", redis.url, redis.pool_size);
}
}
Err(e) => {
eprintln!("Erro ao carregar configuração: {}", e);
std::process::exit(1);
}
}
}
Exemplo dos arquivos por ambiente:
# config/default.toml
[servidor]
host = "127.0.0.1"
porta = 3000
workers = 4
[banco]
url = "postgres://localhost/app_dev"
max_conexoes = 5
[log]
nivel = "debug"
formato = "texto"
# config/producao.toml
[servidor]
host = "0.0.0.0"
porta = 8080
workers = 16
[banco]
url = "postgres://db.prod.internal/app"
max_conexoes = 50
[log]
nivel = "info"
formato = "json"
[redis]
url = "redis://redis.prod.internal:6379"
pool_size = 20
Recursos Avançados
Valores padrão com Default trait
use config::{Config, Environment, File};
use serde::Deserialize;
#[derive(Debug, Deserialize)]
#[serde(default)]
struct ConfigServidor {
host: String,
porta: u16,
workers: usize,
max_body_size: usize,
timeout_segundos: u64,
tls: bool,
}
impl Default for ConfigServidor {
fn default() -> Self {
Self {
host: "127.0.0.1".to_string(),
porta: 3000,
workers: num_cpus::get(),
max_body_size: 10 * 1024 * 1024, // 10MB
timeout_segundos: 30,
tls: false,
}
}
}
#[derive(Debug, Deserialize)]
#[serde(default)]
struct ConfigCache {
habilitado: bool,
ttl_segundos: u64,
max_entradas: usize,
}
impl Default for ConfigCache {
fn default() -> Self {
Self {
habilitado: true,
ttl_segundos: 300,
max_entradas: 10_000,
}
}
}
#[derive(Debug, Deserialize)]
struct Configuracao {
#[serde(default)]
servidor: ConfigServidor,
#[serde(default)]
cache: ConfigCache,
}
fn main() -> Result<(), config::ConfigError> {
// Mesmo sem arquivo de configuração, os defaults funcionam
let config = Config::builder()
.add_source(File::with_name("config/default").required(false))
.add_source(Environment::with_prefix("APP").separator("__"))
.build()?;
let configuracao: Configuracao = config.try_deserialize()?;
println!("{:#?}", configuracao);
Ok(())
}
Configuração com YAML
# config/default.yaml
servidor:
host: "0.0.0.0"
porta: 3000
workers: 4
banco:
url: "postgres://localhost/app"
max_conexoes: 10
pool:
min_idle: 2
max_lifetime_minutos: 30
features:
- nome: "cache"
habilitado: true
- nome: "rate_limit"
habilitado: true
- nome: "beta_ui"
habilitado: false
cors:
origens_permitidas:
- "http://localhost:3000"
- "http://localhost:5173"
metodos_permitidos:
- "GET"
- "POST"
- "PUT"
- "DELETE"
use config::{Config, File, FileFormat};
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct Configuracao {
servidor: ConfigServidor,
banco: ConfigBanco,
features: Vec<Feature>,
cors: ConfigCors,
}
#[derive(Debug, Deserialize)]
struct ConfigServidor {
host: String,
porta: u16,
workers: usize,
}
#[derive(Debug, Deserialize)]
struct ConfigBanco {
url: String,
max_conexoes: u32,
pool: ConfigPool,
}
#[derive(Debug, Deserialize)]
struct ConfigPool {
min_idle: u32,
max_lifetime_minutos: u64,
}
#[derive(Debug, Deserialize)]
struct Feature {
nome: String,
habilitado: bool,
}
#[derive(Debug, Deserialize)]
struct ConfigCors {
origens_permitidas: Vec<String>,
metodos_permitidos: Vec<String>,
}
fn main() -> Result<(), config::ConfigError> {
let config = Config::builder()
.add_source(File::new("config/default", FileFormat::Yaml))
.build()?;
let configuracao: Configuracao = config.try_deserialize()?;
println!("Features habilitadas:");
for f in &configuracao.features {
if f.habilitado {
println!(" - {}", f.nome);
}
}
println!("Origens CORS:");
for origem in &configuracao.cors.origens_permitidas {
println!(" - {}", origem);
}
Ok(())
}
Validação de configuração
use config::{Config, File, Environment};
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct Configuracao {
servidor: ConfigServidor,
banco: ConfigBanco,
jwt: ConfigJwt,
}
#[derive(Debug, Deserialize)]
struct ConfigServidor {
host: String,
porta: u16,
workers: usize,
}
#[derive(Debug, Deserialize)]
struct ConfigBanco {
url: String,
max_conexoes: u32,
}
#[derive(Debug, Deserialize)]
struct ConfigJwt {
secret: String,
expiracao_minutos: u64,
}
impl Configuracao {
fn validar(&self) -> Result<(), Vec<String>> {
let mut erros = Vec::new();
// Validar servidor
if self.servidor.porta == 0 {
erros.push("Porta do servidor não pode ser 0".to_string());
}
if self.servidor.workers == 0 {
erros.push("Número de workers deve ser pelo menos 1".to_string());
}
if self.servidor.workers > 256 {
erros.push("Número de workers não pode exceder 256".to_string());
}
// Validar banco
if self.banco.url.is_empty() {
erros.push("URL do banco de dados é obrigatória".to_string());
}
if !self.banco.url.starts_with("postgres://") {
erros.push("URL do banco deve começar com postgres://".to_string());
}
if self.banco.max_conexoes == 0 {
erros.push("max_conexoes deve ser pelo menos 1".to_string());
}
// Validar JWT
if self.jwt.secret.len() < 32 {
erros.push("JWT secret deve ter pelo menos 32 caracteres".to_string());
}
if self.jwt.expiracao_minutos == 0 {
erros.push("Expiração do JWT deve ser maior que 0".to_string());
}
if erros.is_empty() {
Ok(())
} else {
Err(erros)
}
}
}
fn carregar_e_validar() -> Result<Configuracao, String> {
let config = Config::builder()
.add_source(File::with_name("config/default").required(false))
.add_source(Environment::with_prefix("APP").separator("__"))
.build()
.map_err(|e| format!("Erro ao carregar configuração: {}", e))?;
let configuracao: Configuracao = config
.try_deserialize()
.map_err(|e| format!("Erro ao deserializar configuração: {}", e))?;
configuracao
.validar()
.map_err(|erros| {
format!(
"Configuração inválida:\n{}",
erros.iter().map(|e| format!(" - {}", e)).collect::<Vec<_>>().join("\n")
)
})?;
Ok(configuracao)
}
fn main() {
match carregar_e_validar() {
Ok(config) => {
println!("Configuração carregada e validada:");
println!("{:#?}", config);
}
Err(e) => {
eprintln!("{}", e);
std::process::exit(1);
}
}
}
Acessar valores individuais
use config::{Config, File};
fn main() -> Result<(), config::ConfigError> {
let config = Config::builder()
.add_source(File::with_name("config/default"))
.build()?;
// Acessar valores individuais sem deserializar tudo
let porta: u16 = config.get("servidor.porta")?;
let host: String = config.get("servidor.host")?;
let max_conn: u32 = config.get("banco.max_conexoes")?;
println!("{}:{} (max conn: {})", host, porta, max_conn);
// Verificar se uma chave existe
let tem_redis = config.get::<String>("redis.url").is_ok();
println!("Redis configurado: {}", tem_redis);
// Valor com fallback
let log_level = config
.get::<String>("log.nivel")
.unwrap_or_else(|_| "info".to_string());
println!("Log: {}", log_level);
Ok(())
}
Configuração com secrets de arquivo
use config::{Config, Environment, File};
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct Configuracao {
banco: ConfigBanco,
jwt_secret: String,
api_keys: ApiKeys,
}
#[derive(Debug, Deserialize)]
struct ConfigBanco {
url: String,
}
#[derive(Debug, Deserialize)]
struct ApiKeys {
stripe: String,
sendgrid: String,
}
fn carregar_config() -> Result<Configuracao, config::ConfigError> {
let config = Config::builder()
// Configuração base
.add_source(File::with_name("config/default"))
// Secrets de arquivos (ex: montados como volumes no Docker/K8s)
// /run/secrets/database_url -> banco.url
.set_override_option(
"banco.url",
ler_secret("/run/secrets/database_url"),
)?
.set_override_option(
"jwt_secret",
ler_secret("/run/secrets/jwt_secret"),
)?
.set_override_option(
"api_keys.stripe",
ler_secret("/run/secrets/stripe_key"),
)?
// Variáveis de ambiente (maior prioridade)
.add_source(Environment::with_prefix("APP").separator("__"))
.build()?;
config.try_deserialize()
}
fn ler_secret(caminho: &str) -> Option<String> {
std::fs::read_to_string(caminho)
.ok()
.map(|s| s.trim().to_string())
}
Boas Práticas
1. Estruture os arquivos de configuração
config/
default.toml # Valores padrão para todos os ambientes
dev.toml # Sobrescritas para desenvolvimento
staging.toml # Sobrescritas para staging
producao.toml # Sobrescritas para produção
local.toml # Sobrescritas locais (no .gitignore)
2. Nunca commite secrets
# .gitignore
config/local.toml
config/local.yaml
.env
3. Use um singleton global para a configuração
use config::{Config, Environment, File};
use once_cell::sync::Lazy;
use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct Configuracao {
pub servidor: ConfigServidor,
pub banco: ConfigBanco,
}
#[derive(Debug, Deserialize)]
pub struct ConfigServidor {
pub host: String,
pub porta: u16,
}
#[derive(Debug, Deserialize)]
pub struct ConfigBanco {
pub url: String,
}
pub static CONFIG: Lazy<Configuracao> = Lazy::new(|| {
let ambiente = std::env::var("APP_ENV").unwrap_or_else(|_| "dev".to_string());
Config::builder()
.add_source(File::with_name("config/default").required(false))
.add_source(File::with_name(&format!("config/{}", ambiente)).required(false))
.add_source(Environment::with_prefix("APP").separator("__"))
.build()
.expect("Erro ao construir configuração")
.try_deserialize()
.expect("Erro ao deserializar configuração")
});
// Uso em qualquer lugar:
fn main() {
println!("Porta: {}", CONFIG.servidor.porta);
println!("Banco: {}", CONFIG.banco.url);
}
4. Documente todas as variáveis de configuração
use serde::Deserialize;
/// Configuração completa da aplicação
///
/// ## Variáveis de ambiente
///
/// | Variável | Padrão | Descrição |
/// |---|---|---|
/// | APP_SERVIDOR__HOST | 127.0.0.1 | Host para bind |
/// | APP_SERVIDOR__PORTA | 3000 | Porta HTTP |
/// | APP_BANCO__URL | - | URL de conexão PostgreSQL |
/// | APP_LOG__NIVEL | info | Nível de log (trace/debug/info/warn/error) |
#[derive(Debug, Deserialize)]
pub struct Configuracao {
#[serde(default)]
pub servidor: ConfigServidor,
pub banco: ConfigBanco,
#[serde(default)]
pub log: ConfigLog,
}
#[derive(Debug, Deserialize)]
#[serde(default)]
pub struct ConfigServidor {
/// Host para bind do servidor HTTP
pub host: String,
/// Porta do servidor HTTP (1024-65535)
pub porta: u16,
}
impl Default for ConfigServidor {
fn default() -> Self {
Self {
host: "127.0.0.1".to_string(),
porta: 3000,
}
}
}
#[derive(Debug, Deserialize)]
pub struct ConfigBanco {
/// URL de conexão PostgreSQL
/// Exemplo: postgres://usuario:senha@host:5432/banco
pub url: String,
}
#[derive(Debug, Deserialize)]
#[serde(default)]
pub struct ConfigLog {
/// Nível de log: trace, debug, info, warn, error
pub nivel: String,
/// Formato: texto ou json
pub formato: String,
}
impl Default for ConfigLog {
fn default() -> Self {
Self {
nivel: "info".to_string(),
formato: "texto".to_string(),
}
}
}
5. Integre com Clap para CLI
use clap::Parser;
use config::{Config, Environment, File};
use serde::Deserialize;
#[derive(Parser)]
struct CliArgs {
/// Arquivo de configuração
#[arg(short, long, default_value = "config/default")]
config: String,
/// Ambiente (dev, staging, producao)
#[arg(short, long, env = "APP_ENV", default_value = "dev")]
ambiente: String,
/// Porta do servidor (sobrescreve config)
#[arg(short, long)]
porta: Option<u16>,
}
#[derive(Debug, Deserialize)]
struct Configuracao {
servidor: ConfigServidor,
}
#[derive(Debug, Deserialize)]
struct ConfigServidor {
host: String,
porta: u16,
}
fn main() -> Result<(), config::ConfigError> {
let cli = CliArgs::parse();
let mut builder = Config::builder()
.add_source(File::with_name(&cli.config).required(false))
.add_source(
File::with_name(&format!("config/{}", cli.ambiente)).required(false),
)
.add_source(Environment::with_prefix("APP").separator("__"));
// CLI args sobrescrevem tudo
if let Some(porta) = cli.porta {
builder = builder.set_override("servidor.porta", porta as i64)?;
}
let config = builder.build()?;
let configuracao: Configuracao = config.try_deserialize()?;
println!("Ambiente: {}", cli.ambiente);
println!("Servidor: {}:{}", configuracao.servidor.host, configuracao.servidor.porta);
Ok(())
}
Exemplos Práticos
Sistema de configuração completo para aplicação web
use config::{Config, Environment, File};
use serde::Deserialize;
use std::env;
// === Structs de configuração ===
#[derive(Debug, Deserialize)]
pub struct AppConfig {
pub app: ConfigApp,
pub servidor: ConfigServidor,
pub banco: ConfigBanco,
pub log: ConfigLog,
#[serde(default)]
pub cache: ConfigCache,
#[serde(default)]
pub cors: ConfigCors,
#[serde(default)]
pub email: Option<ConfigEmail>,
}
#[derive(Debug, Deserialize)]
pub struct ConfigApp {
pub nome: String,
pub versao: String,
pub ambiente: String,
}
#[derive(Debug, Deserialize)]
pub struct ConfigServidor {
pub host: String,
pub porta: u16,
pub workers: usize,
#[serde(default = "default_body_limit")]
pub max_body_size_mb: usize,
#[serde(default = "default_timeout")]
pub timeout_segundos: u64,
}
fn default_body_limit() -> usize { 10 }
fn default_timeout() -> u64 { 30 }
#[derive(Debug, Deserialize)]
pub struct ConfigBanco {
pub url: String,
pub max_conexoes: u32,
#[serde(default = "default_min_conexoes")]
pub min_conexoes: u32,
#[serde(default = "default_lifetime")]
pub max_lifetime_minutos: u64,
}
fn default_min_conexoes() -> u32 { 2 }
fn default_lifetime() -> u64 { 30 }
#[derive(Debug, Deserialize)]
#[serde(default)]
pub struct ConfigLog {
pub nivel: String,
pub formato: String,
pub arquivo: Option<String>,
}
impl Default for ConfigLog {
fn default() -> Self {
Self {
nivel: "info".to_string(),
formato: "texto".to_string(),
arquivo: None,
}
}
}
#[derive(Debug, Deserialize)]
#[serde(default)]
pub struct ConfigCache {
pub habilitado: bool,
pub redis_url: Option<String>,
pub ttl_segundos: u64,
}
impl Default for ConfigCache {
fn default() -> Self {
Self {
habilitado: false,
redis_url: None,
ttl_segundos: 300,
}
}
}
#[derive(Debug, Deserialize)]
#[serde(default)]
pub struct ConfigCors {
pub habilitado: bool,
pub origens: Vec<String>,
pub max_age_segundos: u64,
}
impl Default for ConfigCors {
fn default() -> Self {
Self {
habilitado: true,
origens: vec!["*".to_string()],
max_age_segundos: 3600,
}
}
}
#[derive(Debug, Deserialize)]
pub struct ConfigEmail {
pub smtp_host: String,
pub smtp_porta: u16,
pub remetente: String,
pub usuario: String,
pub senha: String,
}
// === Carregamento ===
impl AppConfig {
pub fn carregar() -> Result<Self, config::ConfigError> {
let ambiente = env::var("APP_ENV").unwrap_or_else(|_| "dev".to_string());
let config = Config::builder()
// Defaults programáticos
.set_default("app.nome", "Minha App")?
.set_default("app.versao", env!("CARGO_PKG_VERSION"))?
.set_default("app.ambiente", &ambiente)?
// Camada 1: Configuração base
.add_source(File::with_name("config/default").required(false))
// Camada 2: Configuração do ambiente
.add_source(
File::with_name(&format!("config/{}", ambiente)).required(false),
)
// Camada 3: Configuração local (não commitada)
.add_source(File::with_name("config/local").required(false))
// Camada 4: Variáveis de ambiente
.add_source(
Environment::with_prefix("APP")
.separator("__")
.try_parsing(true),
)
.build()?;
let app_config: AppConfig = config.try_deserialize()?;
// Validar
app_config.validar()?;
Ok(app_config)
}
fn validar(&self) -> Result<(), config::ConfigError> {
if self.servidor.porta == 0 {
return Err(config::ConfigError::Message(
"Porta do servidor não pode ser 0".to_string(),
));
}
if self.banco.url.is_empty() {
return Err(config::ConfigError::Message(
"URL do banco de dados é obrigatória".to_string(),
));
}
Ok(())
}
pub fn is_producao(&self) -> bool {
self.app.ambiente == "producao"
}
pub fn is_dev(&self) -> bool {
self.app.ambiente == "dev"
}
}
fn main() {
match AppConfig::carregar() {
Ok(config) => {
println!("=== {} v{} ({}) ===",
config.app.nome, config.app.versao, config.app.ambiente);
println!("Servidor: {}:{}", config.servidor.host, config.servidor.porta);
println!("Workers: {}", config.servidor.workers);
println!("Banco: {} (max: {} conn)",
config.banco.url, config.banco.max_conexoes);
println!("Log: {} ({})", config.log.nivel, config.log.formato);
println!("Cache: {}", if config.cache.habilitado { "ativo" } else { "inativo" });
println!("Produção: {}", config.is_producao());
}
Err(e) => {
eprintln!("Falha ao carregar configuração: {}", e);
std::process::exit(1);
}
}
}
Comparação com Alternativas
| Característica | config | dotenvy | figment | envy |
|---|---|---|---|---|
| Abordagem | Multi-fonte, layered | Arquivos .env | Multi-fonte | Env vars apenas |
| Formatos | TOML, YAML, JSON, INI | .env | TOML, YAML, JSON | Env vars |
| Layering | Sim (múltiplas fontes) | Não | Sim | Não |
| Type-safe | Sim (via Serde) | Não (strings) | Sim (via Serde) | Sim (via Serde) |
| Env vars | Sim | Sim | Sim | Sim (foco) |
| Validação | Manual | Não | Manual | Manual |
| Complexidade | Média | Baixa | Média | Baixa |
- config vs dotenvy: dotenvy apenas carrega
.envpara variáveis de ambiente. config faz merge de múltiplas fontes. - config vs figment: figment (usado pelo Rocket) é similar em conceito. config é mais popular e tem mais documentação.
- config vs envy: envy deserializa apenas de variáveis de ambiente. config suporta múltiplas fontes.
- Para projetos simples que só precisam de env vars, dotenvy + envy é suficiente. Para configuração completa em camadas, use config.
Conclusão
A crate Config resolve o problema universal de gerenciamento de configuração de forma elegante e Rust-idiomatic. O sistema de camadas permite que a mesma aplicação funcione perfeitamente em desenvolvimento local (com defaults sensatos), staging (com arquivo de configuração), e produção (com variáveis de ambiente e secrets), tudo sem alterar uma linha de código.
A integração perfeita com Serde significa que suas configurações são structs Rust tipadas e verificadas em compile-time, eliminando erros de configuração que só apareceriam em runtime.
Próximos passos
- Combine com Clap para aceitar configuração via linha de comando
- Use Tracing para logar quando configurações são carregadas
- Explore dotenvy para carregar
.envem desenvolvimento - Integre com Axum para injetar configuração como estado da aplicação