Rust: Loading Environment Variables to Typesafe Structs

·

2 min read

Rust: Loading Environment Variables to Typesafe Structs
💡
You can find the source code for this post right over here

Hello Rustaceans!

Are you tired of handling multiple environment variables in a messy way? Let me show you how to load .env files into typesafe structs in Rust, making your code cleaner and more maintainable.

The Traditional Approach

Initially, I used the dotenvy crate for loading .env files like this:

use std::env;

fn main() {
    dotenvy::dotenv().ok();
    let db_url = env::var("DATABASE_URL").expect("DATABASE_URL is not set in .env file");
    println!("DATABASE_URL {:?}", db_url);
}

This method is straightforward but quickly becomes unmanageable with more environment variables.

Learning from TypeScript

As a former TypeScript user, I appreciated how the zod library ensured that all required environment variables were present:

const envSchema = z.object({
  JOB_ID: z.string().min(1),
  CLOUDFLARE_R2_ACCOUNT_ID: z.string().min(1),
  CLOUDFLARE_R2_ACCESS_KEY_ID: z.string().min(1),
  CLOUDFLARE_R2_SECRET_ACCESS_KEY: z.string().min(1),
});

const env = envSchema.parse(process.env);

This approach kept things organized and error-free. So, I wondered, can we achieve something similar in Rust?

The Rust Solution: envy

My search led me to the envy crate, which does exactly what I needed in Rust:

use serde::Deserialize;

#[derive(Deserialize, Debug)]
struct Config {
    database_url: String,
}

fn main() {
    dotenvy::dotenv().ok();
    let config = envy::from_env::<Config>().unwrap();
    println!("DATABASE_URL {:?}", config.database_url);
}

With envy, you can easily map environment variables to a typesafe struct.

Advanced Features and Defaults

envy goes beyond basic deserialization. It supports Option types, Vecs, and more. You can even set default values using serde's attributes:

use serde::Deserialize;

#[derive(Deserialize, Debug)]
struct Config {
    database_url: String,
    #[serde(default = "default_min_block_confirmations")]
    min_block_confirmations: u8,
}

fn default_min_block_confirmations() -> u8 {
    100
}

fn main() {
    dotenvy::dotenv().ok();
    let config = envy::from_env::<Config>().unwrap();
    println!("DATABASE_URL {:?}", config.database_url);
    println!("MIN_BLOCK_CONFIRMATIONS {:?}", config.min_block_confirmations);
}

Prefixing Environment Variables

For those who prefer prefixed environment variables, envy has got you covered:

use serde::Deserialize;

#[derive(Deserialize, Debug)]
struct Config {
    database_url: String,
    #[serde(default = "default_min_block_confirmations")]
    min_block_confirmations: u8,
}

fn main() {
    dotenvy::dotenv().ok();
    let config = envy::prefixed("APP_").from_env::<Config>().unwrap();
    println!("APP_DATABASE_URL {:?}", config.database_url);
    println!("APP_MIN_BLOCK_CONFIRMATIONS {:?}", config.min_block_confirmations);
}

Conclusion

There you have it! Loading .env files into typesafe structs in Rust is simple and efficient with envy. This approach keeps your code neat and your variables organized. Give it a try, and you'll see how it can streamline your Rust projects.

Happy coding, and keep Rust-ing!