Skip to main content
Background Image
  1. Projects/

Premortem - Configuration Validation for Rust

Author
Glen Baker
Building open source tooling
Table of Contents

Find all configuration errors before your application starts, not one at a time in production

Crates.io
Documentation
GitHub

Links: GitHub | Crates.io | Documentation

Overview
#

Premortem is a Rust configuration library that performs comprehensive validation before your application starts. Instead of discovering configuration errors one at a time during execution, premortem reveals all fatal issues upfront - a “premortem” for your configuration.

The Problem
#

Configuration errors are a leading cause of production outages:

  • Sequential failure discovery - Traditional config loading fails on the first error, requiring multiple deploy cycles to find all issues
  • Runtime surprises - Missing or invalid configuration values surface only when code paths are executed
  • Lost provenance - When values come from multiple sources (files, env vars, CLI), it’s unclear where a bad value originated
  • Testing complexity - Configuration loading with I/O makes testing difficult
  • No hot-reload - Configuration changes require application restarts

The Solution
#

Premortem validates your entire configuration upfront and tells you everything that’s wrong in one pass:

use premortem::{Config, Validate};

#[derive(Config, Validate)]
struct AppConfig {
    #[validate(min = 1, max = 65535)]
    port: u16,

    #[validate(non_empty)]
    database_url: String,

    #[validate(min = 1)]
    max_connections: u32,

    #[config(default = 30)]
    timeout_seconds: u32,
}

fn main() {
    // Validates ALL fields, reports ALL errors at once
    match AppConfig::load() {
        Ok(config) => run_app(config),
        Err(errors) => {
            // See every configuration problem, not just the first
            for error in errors {
                eprintln!("{}: {} (from {})",
                    error.field,
                    error.message,
                    error.source);
            }
            std::process::exit(1);
        }
    }
}

Key Features
#

Error Accumulation
#

Collect all validation errors in a single pass:

// Instead of:
// Error: port must be positive
// [fix and redeploy]
// Error: database_url cannot be empty
// [fix and redeploy]
// Error: max_connections must be at least 1

// You get:
// Errors:
//   - port: must be between 1 and 65535 (from: environment)
//   - database_url: cannot be empty (from: config.toml)
//   - max_connections: must be at least 1 (from: default)

Value Provenance Tracking
#

Know exactly where each configuration value came from:

let config = AppConfig::load_traced()?;

// See the source of any value
println!("port = {} (from {})", config.port.value, config.port.source);
// Output: port = 8080 (from environment:PORT)

Layered Configuration
#

Compose configuration from multiple sources with clear precedence:

let config = AppConfig::builder()
    .with_defaults()
    .with_file("config.toml")?
    .with_file("config.local.toml").optional()
    .with_env_prefix("APP")
    .with_cli_args()
    .build()?;

Declarative Validation
#

Express validation rules directly in your type definitions:

#[derive(Config, Validate)]
struct DatabaseConfig {
    #[validate(url, scheme = "postgres")]
    connection_string: String,

    #[validate(range = 1..=100)]
    pool_size: u32,

    #[validate(duration, min = "1s", max = "30s")]
    connect_timeout: Duration,

    #[validate(custom = "validate_ssl_mode")]
    ssl_mode: SslMode,
}

Hot-Reload Support
#

Optionally watch for configuration changes:

let config = AppConfig::load_watched(|new_config| {
    // Called when config files change
    update_runtime_settings(new_config);
})?;

Testable Configuration
#

Abstract I/O through ConfigEnv for easy testing:

#[test]
fn test_config_validation() {
    let env = MockConfigEnv::new()
        .with_var("PORT", "invalid")
        .with_var("DATABASE_URL", "");

    let result = AppConfig::load_with_env(&env);

    assert!(result.is_err());
    let errors = result.unwrap_err();
    assert_eq!(errors.len(), 2);
}

Technical Highlights
#

  • Language: Rust
  • Derive Macros: Config, Validate for declarative configuration
  • Format Support: TOML, JSON, YAML (via feature flags)
  • Serialization: Built on serde for broad compatibility
  • Testing: Mockable I/O layer for unit tests

Installation
#

# Add to Cargo.toml
cargo add premortem

# With format support
cargo add premortem --features toml,json,yaml

# With hot-reload
cargo add premortem --features watch

Quick Start
#

use premortem::{Config, Validate};

#[derive(Config, Validate)]
struct ServerConfig {
    #[validate(min = 1024, max = 65535)]
    port: u16,

    #[validate(non_empty)]
    host: String,

    #[config(default = false)]
    debug: bool,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = ServerConfig::builder()
        .with_file("server.toml")?
        .with_env_prefix("SERVER")
        .build()?;

    println!("Starting server on {}:{}", config.host, config.port);
    Ok(())
}

Project Status
#

Active development, available on crates.io.

Links#

Related

Stillwater - Pure Core, Imperative Shell for Rust
Debtmap - Rust Technical Debt Analyzer
Prodigy - AI Workflow Orchestration for Claude
Automating Documentation Maintenance with Prodigy: A Real-World Case Study