Skip to content
GitHub

Slots

It’s common to define your configuration in a file while also sourcing values from environment variables.

Consider the following configuration file:

// config/base.json
{
  "appURL": "http://localhost:3000",
  "port": 3000
}

Notice that the port number is used in two places. Instead of hardcoding this value, you can use a PORT variable from your environment, for example, in a .env file:

PORT=3000

To reference this environment variable, you can use a “slot”:

// config/base.json
{
  "appURL": "http://localhost:${PORT}",
  "port": "${PORT}"
}

When Layerfig processes your configuration, it finds slots and replaces them with the corresponding environment variable’s value:

$PORT => process.env.PORT => 3000

For convenience, you can also reference values within the same configuration file. This helps avoid duplication and keeps your configuration consistent.

In the previous “port” example, instead of defining the $PORT slot in two places, you can use a self-referencing slot:

// config/base.json
{
  "appURL": "http://localhost:${self.port}",
  "port": "${PORT::-3000}"
}

Layerfig looks for the self.* syntax in the configuration file and uses the value after the dot as an “object path” to find the value in the same configuration. In this case, it will look for the port value.

The final configuration will be:

{
  "appURL": "http://localhost:3000",
  "port": "3000"
}

By default, Layerfig uses $ as the slot prefix, but you can change it by passing the slotPrefix option.

Sometimes, you may want to try multiple environment variables for a single value, using a specific order of priority.

For example, imagine you want to determine the current Git branch. This value could come from different sources:

  1. process.env.GIT_REF
  2. process.env.REF
  3. .branch (from the same configuration file)

To do this, you can use the extended slot syntax, separating each variable name with a colon:

// config/base.json
{
  "branch": "${GIT_REF::REF::self.branch}"
}

Layerfig processes this from left to right, checking for GIT_REF, then REF, and so on, using the first environment variable it finds. If none of the environment variables in the chain are found, Layerfig keeps the original slot string.

In addition to chaining variables, you can also provide a literal fallback value if none of the environment variables are set.

This is done by adding the :- operator to the extended slot syntax. This works even with a single variable:

// config/base.json
{
  "port": "${PORT::-3000}"
}

If the PORT environment variable is not defined, Layerfig will use 3000 as the value.

You can combine this operator with variable chaining for more complex cases:

// config/base.json
{
  "branch": "${GIT_REF::REF::MAIN_REF::-main}"
}

If none of the GIT_REF, REF, or MAIN_REF environment variables are found, the value will fall back to the literal string main.

Here are a few things to keep in mind when working with slots.

If an environment variable for a slot is not defined, Layerfig will not replace it, and the configuration value will keep the original slot string:

config.appURL; // undefined

If your schema expects a defined value, it will throw an error during validation. Also, for array items, if a value gets resolved to undefined, it will be removed.

For example, let’s say only ORIGIN_1 is defined in your environment variables.

ORIGIN_1=*

The following config:

allowOrigins: ["${ORIGIN_1}", "${ORIGIN_2}"]

Will be resolved to:

const config = {
  allowOrigins: ["*"], // ORIGIN_2 was discarded.
};

The runtimeEnv option can be process.env in the server config, import.meta.env in the client, or even a plain object. Node’s process environment is a record Record<string, string|undefined>, but import meta env can hold non-string values such as booleans and numbers.

Layerfig supports all these value types, but in your final configuration, you will most likely have string values in your validate(finalConfig) function.

const finalConfig = {
  appURL: "http://localhost:3000",
  port: "3000",
  dev: "true",
  prod: "false",
};

To handle this, you can use a validation schema to coerce the string value into the type you expect. For example, with Zod:

import { z } from "@layerfig/config";

const schema = z.object({
  appURL: z.string(),
  port: z.coerce.number().positive().int(),
  dev: z.coerce.boolean(),
  prod: z.coerce.boolean(),
});

The z.coerce.* function will parse the string value and transform it into the type you want.