Env Vars in under 100 Lines of Code — (.env)

Env Vars in under 100 Lines of Code — (.env)

Photo by John O’Nolan on Unsplash

Like many projects that run on Node.js our apps use the dotenv package from npm to load environment variables from a local file which is kept out of git.

This lets our developers keep important secrets out of source control while our apps maintain a consistent way to read from the environment.

But I like to break things and decided to rewrite the dotenv entirely in ClojureScript, the result has been added to our degree9/enterprise repo.

Let’s get started!

Ok, so the Node.js environment is mutable within the running process. Which means we can override the js/process.env object with variables we load from our local file, this is basically the same process that dotenv uses.

Let’s start with some simple helper functions, these will make working with the .env file and js/process.env a bit easier.

First up is read-file we want to make sure the environment is populated prior to anything trying to read from it, so we are using the synchronous operation. By providing an encoding the function will return a string instead of a buffer.

(defn- read-file [path]
(.readFileSync fs path #js{:encoding "utf8"}))
(defn- env-file [dir]
(.resolve path dir ".env"))

The env-file is a simple path resolver which will allow us to provide an optional path to look for the .env file.

(defn- split-kv [kvstr]
(cstr/split kvstr #"=" 2))
(defn- split-config [config]
(->> (cstr/split-lines config)
(map split-kv)
(into {})))

Next we have the split-kv helper, this produces a key/value pair from MY_ENV_VAR=foobar .

While split-config takes the result of our synchronous file read then splits each line and applies our previous split-kv to each.

(defn- dot-env [path]
(-> (env-file path)
(read-file)
(split-config)))

The result of our existing helpers is the dot-env function, a convenient way to load our .env files as a hash-map config.

(defn- node-env [env]
(->> (js-keys env)
(map (fn [key] [key (obj/get env key)]))
(into {})))

It has a counter part, the node-env function. This converts a js/process.env object into a clojure hash-map.

Our last helper is populate-env! which takes a clojure map and sets each key value pair to the js/process.env object.

(defn- populate-env! [env]
(doseq [[k v] env]
(obj/set js/process.env k v)))

Now that we have all our helper functions ready to go, we move on to the public API we will be using to interact with the environment variables.

(defn init!
"Initialize environment with variables from .env file."
([] (init! {:path (.cwd js/process) :env js/process.env}))
([{:keys [path env]}]
(populate-env!
(merge (dot-env path)
(node-env env)))))

Starting up our application we want to initialize the environment, we do this by populating it with the result of merging our .env and js/process.env maps. We want to call this init! function as early as possible in our code so that the environment is ready before anything tries to read from it.

In our application we simply read from the environment using our other public function get

(defn get
"Return the value for `key` in environment or `default`."
([key] (get key nil))
([key default] (get js/process.env key default))
([env key default] (obj/get env key default)))

Put it all together!

(require [degree9.env :as env])
(env/init!)
(env/get "MY_ENV_VAR")

Env Vars in under 100 Lines of Code — (.env) was originally published in degree9 on Medium, where people are continuing the conversation by highlighting and responding to this story.