Skip to content
GitHub

Migrate to v3

Layerfig v3 consolidates several learnings from migrating legacy projects to use Layerfig, making configuration more flexible and easier to maintain.

Previously, if you wanted to reuse the same slot value in multiple places, you had to repeat the logic:

{
  "port": "${PORT:-3000}",
  "host": "localhost:${PORT:-3000}" // repeating port logic
}

While this worked, it wasn’t convenient.

With self-referencing slots, you can now reference a value defined in the same configuration:

{
  "port": "${PORT:-3000}",
  "host": "localhost:${self.port}"
}

Layerfig now exports two new submodules:

  • @layerfig/config/zod → Zod v4
  • @layerfig/config/zod-mini → Zod v4 Mini

These are useful if you want to:

  • Keep your schema in a separate file
  • Use Zod across your application without importing it from the main module

Before, if you wanted to point out a different config folder to Layerfig search for files, you would use the configFolder option:

import path from "node:path";

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

The problem was that internally, Layerfig would use process.cwd() and resolve to the folder you’ve specified but in some cases this caused issues.

Now, if you want to change the default folder (or have a different one based on the environment), you can use 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();

Please notice that as the name states, this option expects an absolute path to the folder where Layerfig should look for configuration files. If not it will throw an error.

In v2, creating a custom configuration parser looked like this:

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

export const iniParser = defineConfigParser({
  acceptedFileExtensions: ["ini"],
  parse: (fileContent) => {
    // Logic to fetch, read, and parse the content
    // Should return a Result
  },
});

In v3, you now need to create a class that extends ConfigParser:

// ./path/to/custom-parser.ts
import { ConfigParser } from "@layerfig/config";

class IniParser extends ConfigParser {
  constructor() {
    super({
      acceptedFileExtensions: ["ini"],
    });
  }

  load(fileContent: string) {
    // Logic to fetch, read, and parse the content
    // Should return a Result
  }
}

export const iniParser = new IniParser();

This change improves internal checks and validations within Layerfig.


To better separate client and server code, Layerfig now provides a dedicated client submodule:

import { ConfigBuilder } from "@layerfig/config/client";

All exports from the client submodule are safe for browser environments.
The included z instance is zod/mini, helping reduce your client bundle size.

For server-side code, you can still import everything from the main module:

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

In v2, slots could be defined in a few ways:

  • $PORT => it would get process.env.PORT
  • ${APP_PORT:PORT} => it would try to resolve process.env.APP_PORT and if not found, process.env.PORT
  • ${APP_PORT:PORT:3000} => same but if both not found, fallback to 3000
  • ${self.port} => reference to the .port value of the config

This strategy made unnecessarily complex and error prone to define what is a slot and split the values inside.

Now, every slot needs to be wrapped by ${} (or defining your own slotPrefix, e.g., #{}, @{}, etc.).

-port: $PORT
+port: ${PORT}

Also, when channing values, instead using single colon (:) to separate the values, you must use double colon (::):

-port: ${APP_PORT:PORT:-3000}
+port: ${APP_PORT::PORT::-3000}

This refactor now allow you to specify more complex cases, such as multiple slots in the same value:

host:
  ${HOST::-localhost}:${self.port::-3000}
  # 👆🏽 gets resolved to `localhost:3000` if neither `HOST` nor `self.port` are defined

  • User options are now validated with Zod (Mini)
  • Zod upgraded from v3 to v4