Written by Alex de Sousa
Once an Elixir project is large enough, maintaining config files and configuration variables becomes a nightmare:

Ideally though, configurations should be:
In summary: easy to maintain.
We'll elaborate using the the following example:
config :myapp,
hostname: System.get_env("HOSTNAME") || "localhost",
port: String.to_integer(System.get_env("PORT") || "80")
The previous code is:
hostname and port of what?hostname and port used?Conclusion: it's hard to maintain.
We could mitigate some of these problems with one simple approach:
The following, though a bit more verbose, would be the equivalent to the previous config:
defmodule Myapp.Config do
@moduledoc "My app config."
@doc "My hostname"
def hostname do
System.get_env("HOSTNAME") || "localhost"
end
@doc "My port"
def port do
String.to_integer(System.get_env("PORT") || "80")
end
end
Unlike our original code, this one is:
@doc attribute.However, we still have essentially the same code we had before, which is:
There's gotta be a better way!

Skogsrå is a library for loading configuration variables with ease, providing:
:persistent_term as temporal storage.The previous example can be re-written as follows:
defmodule Myapp.Config do
@moduledoc "My app config."
use Skogsra
@envdoc "My hostname"
app_env :hostname, :myapp, :hostname,
default: "localhost",
os_env: "HOSTNAME"
@envdoc "My port"
app_env :port, :myapp, :port,
default: 80,
os_env: "PORT"
end
This module will have these functions:
Myapp.Config.hostname/0 for retrieving the hostname.Myapp.Config.port/0 for retrieving the port.With this implementation, we end up with:
@envdoc module attribute.Myapp.Config module.app_env options are self explanatory.
Calling Myapp.Config.port() will retrieve the value for the port in the following order:
$PORT.From the configuration file e.g. our test config file might look like:
# file config/test.exs
use Mix.Config
config :myapp,
port: 4000
From the default value, if it exists (In this case, it would return the integer 80).
The values will be casted as the default values' type unless the option type is provided (see Explicit type casting section).
Though Skogsrå has many options and features, we will just explore the ones I use the most:
When the types are not any, binary, integer, float, boolean or atom, Skogsrå cannot automatically cast values solely by the default value's type. Types then need to be specified explicitly using the option type. The available types are:
:any (default).:binary.:integer.:float.:boolean.:atom.:module: for modules loaded in the system.:unsafe_module: for modules that might or might not be loaded in the system.Skogsra.Type implementation: a behaviour for defining custom types.Let's say we need to read an OS environment variable called HISTOGRAM_BUCKETS as a list of integers:
export HISTOGRAM_BUCKETS="1, 10, 30, 60"
We could then implement Skogsra.Type behaviour to parse the string correctly:
defmodule Myapp.Type.IntegerList do
use Skogsra.Type
@impl Skogsra.Type
def cast(value)
def cast(value) when is_binary(value) do
list =
value
|> String.split(~r/,/)
|> Stream.map(&String.trim/1)
|> Enum.map(String.to_integer/1)
{:ok, list}
end
def cast(value) when is_list(value) do
if Enum.all?(value, &is_integer/1), do: {:ok, value}, else: :error
end
def cast(_) do
:error
end
end
And finally use Myapp.Type.IntegerList in our Skogsrå configuration:
defmodule Myapp.Config do
use Skogsra
@envdoc "Histogram buckets"
app_env :buckets, :myapp, :histogram_buckets,
type: Myapp.Type.IntegerList,
os_env: "HISTOGRAM_BUCKETS"
end
Then it should be easy to retrieve our buckets from an OS environment variable:
iex(1)> System.get_env("BUCKETS")
"1, 10, 30, 60"
iex(2)> Myapp.Config.buckets()
{:ok, [1, 10, 30, 60]}
or if the variable is not defined, from our application configuration:
iex(1)> System.app_env(:myapp, :histogram_buckets)
[1, 10, 30, 60]
iex(2)> Myapp.Config.buckets()
{:ok, [1, 10, 30, 60]}
Skogsrå provides an option for making configuration variables mandatory. This is useful when there is no default value for our variable and Skogsrå it's expected to find a value in either an OS environment variable or the application configuration e.g. given the following config module:
defmodule MyApp.Config do
use Skogsra
@envdoc "Server port."
app_env :port, :myapp, :port,
os_env: "PORT",
required: true
end
The function Myapp.Config.port() will error if PORT is undefined and
the application configuration is not found:
iex(1)> System.get_env("PORT")
nil
iex(2)> Application.get_env(:myapp, :port)
nil
iex(3)> MyApp.Config.port()
{:error, "Variable port in app myapp is undefined"}
All the configuration variables will have the correct function @spec definition e.g. given the following definition:
defmodule Myapp.Config do
use Skogsra
@envdoc "PostgreSQL hostname"
app_env :db_port, :myapp, [:postgres, :port],
default: 5432
end
The generated function Myapp.Config.db_port/0 will have the following @spec:
@spec db_port() :: {:ok, integer()} | {:error, binary()}
The type is derived from:
default value (in this case the integer 5432)type configuration value (see the previous Explicit type casting section).Skogsra provides a simple way to handle your Elixir application configurations in a type-safe and organized way. Big projects can certainly benefit from using it.
Hope you found this article useful. Happy coding!

Alex de Sousa
Elixir alchemist. Tech enthusiast.