When writing a web application, the need to thread environment variables into your request handlers is almost always a necessity. For example, access an API secret or some configurable url is commonly required. Working with Axum is no different.

I've waffled on several different approaches to this and I've landed on something quite simple that serves as a convenient way to access environment variables wherever they are needed.

#[derive(Clone, Debug)]
pub struct EnvironmentVariables {
    pub database_url: Cow<'static, str>,
    pub port: u16,
    pub secret: Cow<'static, str>,
    pub app_origin: Cow<'static, str>,
    pub google_client_id: Cow<'static, str>,
    pub google_client_secret: Cow<'static, str>,
}

impl EnvironmentVariables {
    pub fn from_env() -> anyhow::Result<Self> {
        dotenv::dotenv().ok();

        Ok(Self {
            database_url: match dotenv::var("DATABASE_URL") {
                Ok(url) => url.into(),
                Err(err) => bail!("missing DATABASE_URL: {err}"),
            },
            port: match dotenv::var("PORT") {
                Ok(port) => port.parse()?,
                _ => 8000,
            },
            secret: match dotenv::var("SECRET") {
                Ok(secret) => secret.into(),
                Err(err) => bail!("missing SECRET: {err}"),
            },
            app_origin: match dotenv::var("APP_ORIGIN") {
                Ok(app_origin) => app_origin.into(),
                Err(err) => bail!("missing APP_ORIGIN: {err}"),
            },
            google_client_id: match dotenv::var("GOOGLE_CLIENT_ID") {
                Ok(google_client_id) => google_client_id.into(),
                Err(err) => bail!("missing GOOGLE_CLIENT_ID: {err}"),
            },
            google_client_secret: match dotenv::var("GOOGLE_CLIENT_SECRET") {
                Ok(google_client_secret) => google_client_secret.into(),
                Err(err) => bail!("missing GOOGLE_CLIENT_SECRET: {err}"),
            },
        })
    }
}

This struct EnvironmentVariables represents every environment variable that can or must be present when running the program. dotenv is used to optionally parse environment variables out of a .env file. This is useful for working in development environments. I find this pattern is useful in every program and is adapted from a pattern that emerged at Strobe (thanks djc).

With Axum, it's possible attach a state object to be made available in every request handler. We can use this to make the EnvironmentVariables state available as well.

#[derive(Clone)]
pub struct AppState {
    pub pool: PgPool,
    pub env: EnvironmentVariables,
    pub client: reqwest::Client,
}

impl AppState {
    pub async fn from_env() -> anyhow::Result<Self> {
        let env = EnvironmentVariables::from_env()?;
        Ok(Self {
            pool: PgPool::connect(&env.database_url).await?,
            env: EnvironmentVariables::from_env()?,
            client: reqwest::Client::new(),
        })
    }
}

Now, when you want to register your app's state, you can simply do this:

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let state = AppState::from_env().await?;
    
    let listener =
        TcpListener::bind(SocketAddr::from((Ipv4Addr::UNSPECIFIED, state.env.port))).await?;
    let app = Router::new()
        .route("/", get(index))
        .with_state(state);

    axum::serve(listener, app).await?;

    Ok(())
}

And now, in every request handler, retrieving your state is as simple as this:

async fn index(State(state): State<AppState>) -> impl IntoResponse {
    sqlx::query("SELECT 1")
        .execute(&state.pool)
        .await
        .map_err(|e| {
            StatusCode::INTERNAL_SERVER_ERROR
        })
        .map(|_| StatusCode::OK)
}

I hope this helps you out. If you have any questions, let me know in the comments below.

Handling environment variables with Axum