██▓     █    ██  ██ ▄█▀ ███▄ ▄███▓     ▓█████▄ ▓█████ ██▒   █▓
▓██▒     ██  ▓██▒ ██▄█▒ ▓██▒▀█▀ ██▒     ▒██▀ ██▌▓█   ▀▓██░   █▒
▒██░    ▓██  ▒██░▓███▄░ ▓██    ▓██░     ░██   █▌▒███   ▓██  █▒░
▒██░    ▓▓█  ░██░▓██ █▄ ▒██    ▒██      ░▓█▄   ▌▒▓█  ▄  ▒██ █░░
░██████▒▒▒█████▓ ▒██▒ █▄▒██▒   ░██▒ ██▓ ░▒████▓ ░▒████▒  ▒▀█░  
░ ▒░▓  ░░▒▓▒ ▒ ▒ ▒ ▒▒ ▓▒░ ▒░   ░  ░ ▒▓▒  ▒▒▓  ▒ ░░ ▒░ ░  ░ ▐░  
░ ░ ▒  ░░░▒░ ░ ░ ░ ░▒ ▒░░  ░      ░ ░▒   ░ ▒  ▒  ░ ░  ░  ░ ░░  
  ░ ░    ░░░ ░ ░ ░ ░░ ░ ░      ░    ░    ░ ░  ░    ░       ░░  
    ░  ░   ░     ░  ░          ░     ░     ░       ░  ░     ░  
                                     ░   ░                 ░   

Clap is perfect for configuring a web service

2025-06-15

While building a Rust web service, I wanted to better way to configure it on both production and my local development environment. In the development environment, I just want reasonable default values with the ability to override them with command-line arguments if I need to. But on production, configuration should propagate itself through environment variables.

The problem was that I could not find a crate that would do this both effectively and in an ergonomic way. Just as I was about to begin writing my own, I realised that the clap crate lets you add an env flag to each derive attribute. So Clap isn't just for parsing command-line arguments; by deriving Parser on my config struct and setting env and default_value attributes, I got a clean and predictable precedence: command-line > env var > default.

Here's a simple example:

use clap::Parser;

#[derive(Parser)]
#[command(name = &"My HTTP Server")]
#[command(version, about, long_about = None)]
struct Config {

	/// Designates HOST IP address for the API
	#[arg(long, env, default_value = &"127.0.0.1")]
	pub api_ip: std::net::IpAddr,

	/// Designates HOST port for the API.
	#[arg(long, env, default_value_t = 58868, value_parser = clap::value_parser!(u16).range(1..))]
	pub api_port: u16,

	/// List whitelisted IP addresses
	///
	/// Some API endpoints require elevated privileges, and should only come from certain IP
	/// addresses. The format should be a comma-separated string of valid IP addresses, for example;
	/// 127.0.0.1,0.0.0.0
	#[arg(long, env, num_args = 0.., value_delimiter = ',', default_value = &"127.0.0.1")]
	pub api_ip_whitelist: Vec<std::net::IpAddr>,

}

Then I could just call parse on the struct.

let config = Config::parse();

Easy. This gave me the flexibility to define values on production with environment variables (like in a container), override them with CLI args, and fall back to sensible defaults for development, all with minimal code.

Clap crate