API reference / @backpack/config
@backpack/config
⚙️️
Type-safe configuration management using Zod.
Installing
First, ensure you have set up Nexus as private registry needed to use Backpack.
Then, install the package using npm (along with Zod):
npm install @backpack/config zod
Usage
Step 1: define your config schema
First, create a schema using Zod. You can use any of Zods features (such as transforms and default values), as long as the top-level data structure is an object.
Example:
import { z } from "zod";
export const Config = z.object({
appName: z.string(),
clients: z.object({
foo: z.object({
baseUrl: z.string().url(),
timeoutMilliseconds: z.number().default(200),
}),
bar: z.object({
baseUrl: z.string().url(),
timeoutMilliseconds: z.number().default(200),
}),
})
});
Step 2: load your config
To load your actual config, you can use one or more loaders.
If multiple loaders are provided, it will deep-merge the data from all loaders, with the last loader taking precedence. If any loader fails, it will still return the data from other loaders.
import { loadConfig, envLoader, yamlLoader, importLoader } from "@backpack/config";
const myConfig = await loadConfig({
schema: Config,
loaders: [
// loads from environment variables
envLoader(),
// loads from a YAML file
yamlLoader("path/to/config.yaml"),
// you need to implement your own environment/profile detection
// to load from a specific source:
yamlLoader(`path/to/config-${profile}.yaml`),
// loads from a TypeScript file, using a dynamic import:
importLoader(() => import("./path/to/config.ts")),
],
});
Step 3: specify your actual config files
Make sure the sources of your loaders are provided according to your schema:
Example using YAML:
appName: My app
clients:
foo:
baseUrl: https://path.to/foo
timeoutMilliseconds: 500
# ...
Example using TypeScript:
// (make sure to use a default export)
export default {
appName: "My app",
clients: {
foo: {
baseUrl: "https://path.to/foo",
timeoutMilliseconds: 500,
}
// ...
}
}
Advanced
Type-safety
You can leverage your Zod-schema for additional type-safety.
First, use the z.infer<>
helper to create a type-alias:
import { z } from "zod";
export type Config = z.infer<typeof Config>;
export const Config = z.object({
// ...
});
Next, use the satisfies
operator to get additional type-checking.
import type { Config } from "path/to/config.ts";
export default {
appName: "My app",
clients: {
foo: {
baseUrl: "https://path.to/foo",
timeoutMilliseconds: 500,
}
// ...
}
} satisfies Config;
If you are using multiple loaders to combine several partial configs, you can use DeepPartial<Config>
to make all properties optional. Alternatively, you can use the defineConfig()
helper - which accepts partial configs by default:
import { defineConfig } from "@backpack/config";
import type { Config } from "path/to/config.ts";
export default defineConfig<Config>({
// app name is defined in a different config file.
clients: {
foo: {
baseUrl: "https://path.to/foo",
timeoutMilliseconds: 500,
}
// ...
}
});
Passing context
In some cases, you may want to pass additional context to your config file. For example, to access certain things from your application context, such as an injection container.
To do this, you can use the importLoader()
like this:
import { loadConfig, envLoader, yamlLoader, importLoader } from "@backpack/config";
export type Context = { /* ... */ }
const myConfig = await loadConfig({
schema: Config,
loaders: [
importLoader({
read: () => import("./path/to/config.ts"),
context: { /* ... */ }
}),
],
});
Now, you can specify your TypeScript config as an arrow function:
import { defineConfig } from "@backpack/config";
import type { Config } from "path/to/config.ts";
export default defineConfig<Config, Context>((context) => ({
// ...
}));
Migrating from @ns/cdk-helper
If you are using @ns/cdk-helper
, you may want to migrate your project to use @backpack/config
instead.
This will bring more type-safety to your CDK config, especially when you are using additional properties. It also provides improved runtime safety using schema validation, making sure you have the right values in your YAML files.
1. Create a Zod schema for your CDK config
Add a Zod schema representing your current configuration model:
import { z } from "zod";
export type BuildConfig = z.infer<typeof BuildConfig>;
export const BuildConfig = z.object({
App: z.string(),
AwsAccount: z.string(),
AwsRegion: z.string(),
Environment: z.enum(["dev", "stg", "prd"]),
Tags: z.record(z.string(), z.string()),
});
TIP
This example uses the same naming convention as @ns/cdk-helper
. Feel free to use a different naming convention (e.g. "camelCase" over "PascalCase") for your property keys.
The @backpack/config
library provides lenient matching, accepting different casing in your config files. If you do so, remember to update all references in your code.
2. Replace your config loading logic
Change your existing code:
import { App } from "aws-cdk-lib";
import { Config } from "@ns/cdk-helper";
const cdk = new App();
const buildConfig = Config(cdk);
// ...
Replace this with the loadConfig()
function from @backpack/config
. You can use the cdkLoader()
to load both YAML files and CDK context variables.
import { App } from "aws-cdk-lib";
import { loadConfig, cdkLoader } from "@backpack/config";
import { BuildConfig } from "../config";
const cdk = new App();
const buildConfig = await loadConfig({
schema: BuildConfig,
loaders: [
cdkLoader(cdk, { configDirectory: "./cdk/config" }),
]
});
// ...
3. Optional: apply envSuffix
where needed
If you are using the envSuffix
from @ns/cdk-helper
somewhere in your project, make sure to explicitly apply it where you need it.
First, add it to your config schema. This will automatically pick up -c envSuffix=foo
as CDK command-line argument.
You can use Zod transforms to make sure it converts to a certain case, such as kebab-case:
import { z } from "zod";
import { kebabCase } from "change-case";
export type BuildConfig = z.infer<typeof BuildConfig>;
export const BuildConfig = z.object({
// ...
envSuffix: z.string()
.transform(it => kebabCase(it))
.optional(),
});
Everywhere you are relying on the App
property, and you want to apply the suffix, make sure to do so explicitly.
For example:
// add the suffix if needed:
const stackPrefix = buildConfig.envSuffix
? `${buildConfig.App}-${buildConfig.envSuffix}`
: `${buildConfig.App}`;
// apply it:
const appStack = new AppStack(cdk, `${stackPrefix}-app-stack`, {
env: {
region: buildConfig.AWSRegion,
account: buildConfig.AWSAccount,
},
buildConfig: buildConfig,
tags: buildConfig.Tags,
// ...
});
4: Double-check your tags
In contrast to @ns/cdk-helper
, the @backpack/config
library contains no CDK-specific logic such as validating CDK tags. If you still want to validate your tags, you can leverage Zod validation for this, by adjusting your schema:
export const BuildConfig = z.object({
// ...
Tags: z.object({
App: z.string(),
Team: z.string(),
// ...
})
});