Sitemap

Typesafe, Validated Configuration in TypeScript (and why I built zodified-config)

--

Press enter or click to view image in full size
Example terminal with a config error by ChatGPT

If you’ve had a deployment succeed but act strangely, chances are an incorrect config was to blame.

Config bugs are tricky because they usually pass code review (they’re seen as harmless config changes), they often will deploy successfully, and secrets in configs make debugging even harder because you can’t safely log them for review.

The thing is, if we are using TypeScript, we get a false sense of security, as where we have strict typing everywhere else, config becomes a quiet loophole where we read config through a function that effectively returns any.

In this post, I will cover:

  • Why Typesafe Config Matters
  • Why validating configuration at application start is a big deal.
  • How zodified-config solves both problems using Zod
  • Examples you can copy into your own project
  • a quick wrap-up

Why typesafe configuration matters (and what can go wrong without it)

A common pattern in Node.js apps is using a config library like node-config:

import config from 'config';

const port = config.get('port');

The problem is that config.get() is basically untyped in your app. In many cases, it returns any or unknown, so TypeScript can’t help you catch mistakes.

Let’s say you expect port to be a number:

import config from 'config';

const port: number = config.get('port');

This compiles, but if the port in the config is “3000” (a string), app.listen(port) may fail.

A more subtle example might be the following.

import config from 'config';

const enableMetrics = config.get('metrics.enabled');

if (enableMetrics) {
startMetricsServer();
}

If metrics.enabled is the string “false”, enableMetrics is truthy, and metrics run unintentionally.

These examples show how easily configuration issues can lead to scattered, confusing bugs.

With that foundation set, let’s move to the importance of validating configuration right at application start.

Beyond simply having typesafe configuration, we also want to ensure that the configuration our application uses is valid. Otherwise, our application starts, but when it receives its first request, it tries to read a config value it needs and fails to do so. At best, only that request is affected; however, in some cases, this could even crash the entire app.

There’s a simple rule I like to follow:

If the app can’t run correctly without a config setting, fail fast before you start serving traffic.

This especially works well when you are working with tools like Kubernetes, where you can continue serving traffic from the old pods while deploying the new ones, and a pod is only healthy when it has a valid configuration.

Validating configuration on startup therefore gives you a bunch of practical wins:

1) Prevent broken deployments

If a required value is missing (API key, database URL, queue name, etc.), you’d rather the app crash immediately than limp along and fail only when a particular code path is hit.

2) Faster feedback loops

Startup validation makes misconfiguration obvious in logs. No need to wait for a user to hit the bug.

3) Clear error messages

Schema validation can tell you exactly what’s wrong:

  • Which key is missing
  • Which key has the wrong type
  • Which nested value failed validation

Introducing zodified-config: typesafe config validated by Zod 🎉

Now that you understand why we need typesafe config and the ability to validate it at application start, I want to introduce you to a library called zodified-config.

zodified-config enables you to:

  • Define your configuration schema using Zod.
  • Define your configuration files as JSON in a top-level config directory.
  • Validate at startup (so you fail fast)
  • Enable TypeScript types for config access across your app.

zodified-config is built as a wrapper around node-config, so it supports all its behaviours, with the additional type safety provided by Zod.

How it to use zodified-config (step-by-step)

1) Installing the library

To get started, you will want to install the Node.js library from NPM. It is available in two versions. If you are using Node.js with the CommonJS module system, you should install the standard version of the library:

npm i zodified-config

Alternatively, if your project is using ECMAScript modules (ESM), you should use the ESM module version of the library:

npm i zodified-config-esm

2) Define your schema

Create a schema that describes the shape of your config.

// src/schema.ts
import z from 'zod';

export const configSchema = z.object({
value: z.string(),
});

export type Config = z.infer<typeof configSchema>;

This is the heart of the approach: your schema is the runtime contract, and z.infer gives you compile-time types derived from the same source.

3) Define your configuration

You now can define your config, as zodified-config is a wrapper arround node-config you define your configuration files the same way, you create a top level folder called config and you create a json file for each environment, with default configuration living in a file called default.json

// config/default.json
{
"value": "hello world"
}

4) Validate your config at startup

Then you tell zodified-config what your validated config looks like using TypeScript module augmentation and validate on boot.

// src/index.ts
import config from 'zodified-config';
import { configSchema } from './schema';
import type { Config } from './schema';

declare module 'zodified-config' {
interface ValidatedConfig extends Config {}
}

try {
config.validate(configSchema);
} catch (error: unknown) {
// If you enter here, the config is invalid (or something else went wrong)
// Check the error message for details
throw error;
}

At this point, the app can confirm config validity before running. If it is incorrect, you will get an array of errors, which you can safely log for debugging later.

5) Access values with type safety

Anywhere in your app, you are now able to import the library and call config.get to retrieve the value:

// src/other.ts
import config from 'zodified-config';

const value = config.get('value');
// value is typed as a string

console.log(value)
// logs "hello world"

The key advantage here is that your codebase now gets typed config, and in most cases, your text editor or IDE will autocomplete config keys, making it easier not to make mistakes.

A more realistic example

Here’s what this looks like in a typical service config shape:

// src/schema.ts
import z from 'zod';

export const configSchema = z.object({
env: z.enum(['development', 'test', 'production']),
port: z.number(),
database: z.object({
url: z.string().url(),
poolSize: z.number().int().positive(),
}),
features: z.object({
metrics: z.boolean(),
}),
});

export type Config = z.infer<typeof configSchema>;

After validating at startup, you can safely use config throughout your app.

const port = config.get('port');                  // number
const dbUrl = config.get('database.url'); // string
const metricsEnabled = config.get('features.metrics'); // boolean

If someone breaks the config in an environment:

  • The app fails immediately.
  • You get a clear validation error.
  • You don’t ship a “looks healthy but is doomed” deployment.

Conclusion

TypeScript provides a lot of safety, but configuration is often the escape hatch through which untyped values can creep back in and quietly undermine it.

Having a schema defined for your config allows you to make it type safe, allowing you to catch misspelt config key names in your editor and at build time.

Then by validating config at startup you are able to:

  • Prevent broken deployments.
  • makes issues obvious in logs and CI/CD
  • reduces “works on my machine” surprises
  • gives you confidence when changing config over time

Tools like zodified-config make this process easy, so your code and your config always stay in sync. In the end, it’s a small change that makes a big difference in your project’s reliability.

--

--

Jonathan Fielding
Jonathan Fielding

Written by Jonathan Fielding

Principal Engineer working for @Legend, speaker about web things, writing about tech, contributor to open source. If you like what I write make sure to follow.