This episode discusses Rust application configuration with config, dotenv, or figment, data versioning and type compatibility when schemas change, as well as the practice of separating config per environment for dev, staging, and production.

A flexible application does not hard-code configuration inside the code. Database hosts, keys, and feature flags should be changeable without recompilation. Episode 9 discusses how a Rust application reads configuration from files and the environment, as well as how data survives schema changes.
You will use the config and figment crates to combine many configuration sources, dotenv for .env files, understand API data versioning, and apply per-environment config separation. These are mandatory skills before deploying an application to production.
Environment variables are the simplest and most idiomatic way to configure applications in the cloud. A .env file makes local development easier:
DATABASE_URL=postgres://user:pass@localhost/db
PORT=8080
LOG_LEVEL=infodotenvy loads this file into the process environment:
cat > src/main.rs <<'EOF'
use std::env;
fn main() {
dotenvy::dotenv().ok();
let port: u16 = env::var("PORT")
.unwrap_or_else(|_| "8080".to_string())
.parse()
.unwrap();
println!("server berjalan di port {}", port);
}
EOF
cargo rundotenvy::dotenv() loads .env into the environment. env::var("PORT") reads the value, with a fallback of 8080 if it is absent. Never commit .env to the repository — use .env.example as the template.
For complex hierarchies, the config crate combines files, environment, and defaults:
cat > src/main.rs <<'EOF'
use config::{Config, File, Environment};
fn main() {
let config = Config::builder()
.set_default("host", "127.0.0.1").unwrap()
.add_source(File::with_name("config").required(false))
.add_source(Environment::with_prefix("APP"))
.build()
.unwrap();
let host: String = config.get("host").unwrap();
println!("host: {}", host);
}
EOF
cargo runThe order of sources determines priority: defaults rank lowest, the environment highest. Environment::with_prefix("APP") reads variables such as APP_HOST. Configuration is collected once at program start and shared as a struct.
figment is a configuration library used by frameworks such as Rocket and the Axum ecosystem. The principle is the same: several layers are merged with a clear priority.
let config: Config = Figment::new()
.merge(Toml::file("config.toml"))
.merge(Env::prefixed("APP_"))
.extract()?;Env::prefixed("APP_") reads environment variables prefixed with APP_. The last layer overrides the previous ones. figment is the choice when you need direct deserialization into a struct with serde.
Data stored for years will face schema changes: new fields, renames, or type changes. Stored databases and files must remain readable by both old and new applications. Two main strategies: migration for databases and payload versioning for encoded data.
Migrations record schema changes in an ordered, idempotent way:
cargo install sqlx-cli --features postgres
sqlx migrate add tambah_kolom_emailFill in the generated migration file with SQL, then apply it:
ALTER TABLE pengguna ADD COLUMN email TEXT;Migrations are recorded in a dedicated table, so each runs only once. sqlx migrate run ensures the schema is consistent across all environments — that is half of the schema evolution work.
For encoded data such as JSON in a database or events in a message queue, add a version field. New applications read old data (forward compatibility), and old applications safely reject data they do not recognize:
cat > src/main.rs <<'EOF'
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct Event {
v: u32,
nama: String,
}
fn main() {
let lama = r#"{"v":1,"nama":"deploy"}"#;
let event: Event = serde_json::from_str(lama).unwrap();
println!("event v{}: {}", event.v, event.nama);
}
EOF
cargo runThe v field marks the payload version. When the schema changes, the version is bumped and the parser adjusts. Serde also supports #[serde(default)] and #[serde(alias)] for optional new fields and renames without breaking old data.
One codebase, many environments: values that are the same across environments go into the base config, while different ones are overridden per environment. The common convention in Rust:
host = "0.0.0.0"
port = 8080
log_level = "info"log_level = "warn"The config crate picks the file based on the environment: APP_ENV=production cargo run sets the environment when running the application, and the loading logic merges the default file, the environment-specific file, and environment variable overrides. This pattern keeps dev, staging, and production on the same code with different values.
Key takeaways:
.env files.dotenvy loads .env; config and figment combine many sources.In the next episode 10 we will discuss basic networking and the HTTP server — building a server with axum, warp, actix-web, or hyper, request-response basics, routing, middleware, and error handling, as well as TCP and UDP connections. Your application starts talking to the outside world.