Skip to content

Configuration

The server ConfigBuilder accepts the following options. Only validate is required.

Option Default Purpose
validate Validates and types the merged config
absoluteConfigFolderPath <process.cwd()>/config Where FileSource looks for files
parser simple JSON parser How file contents are parsed
runtimeEnv process.env Where slots and env vars are read from
slotPrefix "$" The prefix that marks a slot

The ConfigBuilder constructor requires a validate function. This function receives two arguments: the final merged configuration and a Zod instance (v4). It must return the validated configuration.

import { ConfigBuilder, FileSource } from "@layerfig/config";

// config will be type `z.output<typeof schema>` (the actual object type)
export const config = new ConfigBuilder({
  validate: (finalConfig, z) => {
    const schema = z.object({
      appURL: z.url(),
      port: z.coerce.number().int().positive(),
    });

    return schema.parse(finalConfig);
  },
})
  .addSource(new FileSource("base.json"))
  .build();

You can also use other validation libraries. Return the result of the schema validation from the validate function, and the config object will be typed accordingly:

import { ConfigBuilder, FileSource } from "@layerfig/config";
import * as v from "valibot";

export const configSchema = v.object({
  appURL: v.pipe(v.string(), v.url()),
});

export const config = new ConfigBuilder({
  validate: (finalConfig) => v.parse(configSchema, finalConfig),
})
  .addSource(new FileSource("base.json"))
  .build();

Default: '<process.cwd()>/config'

By default, file-based sources added via addSource are loaded from a config folder in the project’s root. You can customize this path using the absoluteConfigFolderPath option:

import path from "node:path";

const config = new ConfigBuilder({
  validate: (finalConfig) => schema.parse(finalConfig),
  absoluteConfigFolderPath: path.resolve(
    process.cwd(),
    "./path/to/config-folder"
  ),
})
  .addSource(new FileSource("base.json"))
  .build();

In this example, the library will look for <root>/path/to/config-folder/base.json.

Default: simple json parser

This option allows you to define a custom parser for non-JSON file types, such as .yaml, .jsonc, or .toml.

import yamlParser from "@layerfig/parser-yaml";

const config = new ConfigBuilder({
  validate: (finalConfig) => schema.parse(finalConfig),
  parser: yamlParser,
})
  .addSource(new FileSource("base.yaml"))
  .build();

In this case, the library will look for <root>/config/base.yaml and load it.

Default: process.env

The object Layerfig reads environment values from. It is used in two places: to resolve slots such as ${PORT}, and as the input to EnvironmentVariableSource.

You normally leave it alone on the server. Override it when the environment does not live on process.env, or when you want to control it explicitly:

// Deno — process.env is a partial shim, so pass the real environment
const config = new ConfigBuilder({
  validate: (finalConfig) => schema.parse(finalConfig),
  runtimeEnv: Deno.env.toObject(),
})
  .addSource(new FileSource("base.json"))
  .build();
// Cloudflare Workers — the environment arrives per request
export function buildConfig(env: Env) {
  return new ConfigBuilder({
    validate: (finalConfig) => schema.parse(finalConfig),
    runtimeEnv: env,
  })
    .addSource(new FileSource("base.json"))
    .build();
}

Passing an explicit object is also how you build a configuration in a test without touching the real environment — see the Testing guide.

Default: "$"

A string that identifies placeholders to be replaced with environment variables.

By default, Layerfig looks for placeholders prefixed with $. You can customize this prefix to avoid conflicts or to match a team’s convention.

For example, to use a double underscore (__) as the prefix:

const config = new ConfigBuilder({
  validate: (finalConfig) => schema.parse(finalConfig),
  slotPrefix: "__",
})
  .addSource(new FileSource("base.json"))
  .build();

Layerfig will now look for placeholders like __{PORT} in your configuration files.

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

Assuming the PORT environment variable is set, the resolved configuration will be:

config.appURL; // "http://localhost:3000"
config.port; // "3000" — a string, unless your schema coerces it