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
Self-Referencing Slots
Section titled “Self-Referencing Slots”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"
}
Custom Slot Prefix
Section titled “Custom Slot Prefix”By default, Layerfig uses $
as the slot prefix, but you can change it by passing the slotPrefix
option.
Chaining Environment Variables
Section titled “Chaining Environment Variables”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:
process.env.GIT_REF
process.env.REF
.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.
Literal Fallbacks
Section titled “Literal Fallbacks”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
.
Caveats
Section titled “Caveats”Here are a few things to keep in mind when working with slots.
Undefined values
Section titled “Undefined values”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.
};
Non-string primitive values
Section titled “Non-string primitive values”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.