Strategy for accessing Node environment variables

I am trying to work out a nice way to access and use Node environment variables.

Specifically I’m assuming that all variables are required and cannot have defaults.

I’d be grateful for feedback on this approach and if there was a nicer way.

First, a binding nodeEnv to access the node process.env global:

@bs.val @bs.scope("process") external nodeEnv: Js.Dict.t<string> = "env"

Next, declare a type for the environment variables we need.

type config = {
  varA: string,
  varB: string,
  varC: string,
}

I’m planning to write a function to collect all of the environment variables and use a Result to indicate success or error:

type configResult = Ok(config) | Error(string)

Next, create a helper function to get environment variable, and raise an exception if the variable is not found.

exception EnvironmentVariableMissing(string)

let env = name => {
  switch nodeEnv->Js.Dict.get(name) {
  | Some(value) => value
  | None => raise(EnvironmentVariableMissing(name))
  }
}

Lastly write the function that collects the environment variables.

let getConfig = (): configResult => {
  try {
    let varA = env("VAR_A")
    let varB = env("VAR_B")
    let varC = env("VAR_C")
    Ok({
      varA: varA,
      varB: varB,
      varC: varC,
    })
  } catch {
  | EnvironmentVariableMissing(name) => Error("Environment variable '" ++ name ++ "' is missing")
  }
}

This seems to be working well, but I am wondering if there is a better strategy that does not need to raise and catch exceptions?

Thanks for any suggestions.

1 Like

A few suggestions: there is already a binding for node.process.env:

let nodeEnv = Node.Process.process["env"]

Also, you don’t need a custom type configResult, the built-in result<'a, 'e> type does what you need.

You can write the env function as:

let env = name =>
  switch Js.Dict.get(nodeEnv, name) {
  | Some(value) => Ok(value)
  | None => Error(`Environment variable ${name} is missing`)
  }

This has the benefit that it returns a result<string, string> straightaway instead of throwing an exception. You can then switch on this function multiple times in a single switch expression to build your config record in one shot:

let getConfig = () =>
  switch (env("VAR_A"), env("VAR_B"), env("VAR_C")) {
  // Got all vars
  | (Ok(varA), Ok(varB), Ok(varC)) => Ok({varA, varB, varC})

  // Did not get one or more vars, return the first error
  | (Error(_) as err, _, _)
  | (_, Error(_) as err, _)
  | (_, _, Error(_) as err) => err
  }
6 Likes

@yawaramin very nice, thank you :pray: