If you've ever had a deployment succeed but then the service acts weird, there's a decent chance config was the culprit.
Config bugs are painful because:
- they often pass code review ("it's just config")
- deployments can look healthy until the broken path is hit
- secrets make debugging harder (you can't safely log everything)
- and TypeScript can give a false sense of safety because config is often a quiet loophole where
anycreeps back in
In this post:
- why typesafe config matters (and what goes wrong without it)
- why validating config at startup is a big deal
- how zodified-config solves both using Zod
- copy/paste examples you can drop into your app
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 issue is that config.get() is often effectively untyped inside your app. In many setups it returns any (or unknown), which means TypeScript can't catch mistakes.
For example, you expect port to be a number:
import config from "config";
const port: number = config.get("port");
This compile but if your config value is "3000" (a string), you may run into runtime problems when you do:
app.listen(port);
Even worse are subtle bugs:
import config from "config";
const enableMetrics = config.get("metrics.enabled");
if (enableMetrics) {
startMetricsServer();
}
If metrics.enabled is the string "false", it's truthy, and suddenly metrics are enabled in production when they shouldn't be.
That's how config issues turn into scattered, confusing “ghost bugs”.
Why validating configuration at startup is a big deal
Types help you use config safely. Validation helps you ensure the config is actually valid before the app starts serving traffic.
Without startup validation you often get this pattern:
- app boots “fine”
- first request hits a codepath that needs config
- runtime explosion
- sometimes the service crashes entirely
A rule I like:
If the app can't run correctly without a config setting, fail fast before you start serving traffic.
This is especially useful in Kubernetes: new pods should only become “ready” if their config is valid.
Practical wins of startup validation
Prevent broken deployments
Missing values (DB URLs, API keys, queue names) should crash immediately, not halfway through a request.Faster feedback loops
Misconfiguration becomes obvious in startup logs (and CI/CD), rather than waiting for a user to trigger it.-
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
Once you want typesafe config and startup validation, you end up wanting the same workflow everywhere:
- define a schema (runtime contract)
- validate config once, on boot
- access config with types and autocomplete across your app
That's what zodified-config is for.
It lets you:
- define your config schema using Zod
- keep using the familiar
node-configfile structure (config/default.json, etc.) - validate at startup (fail fast)
- get TypeScript types for config access everywhere
It's built as a wrapper around node-config, so you keep existing behaviour just with Zod-backed validation and type safety.
How to use zodified-config (step-by-step)
1) Install
If you're using CommonJS:
npm i zodified-config
If you're using ESM:
npm i zodified-config-esm
2) Define your schema
// src/schema.ts
import z from "zod";
export const configSchema = z.object({
value: z.string(),
});
export type Config = z.infer<typeof configSchema>;
This schema is the runtime contract, and z.infer gives you compile-time types from that same source.
3) Define your configuration files
Because this wraps node-config, you use the same structure:
// config/default.json
{
"value": "hello world"
}
4) Validate at startup (fail fast)
Declare your validated config type using TypeScript module augmentation, then 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 {}
}
config.validate(configSchema);
If config is invalid, validation throws at startup, exactly when you want to find out.
(You can wrap it in a try/catch if you want to customise the error output.)
5) Access values with type safety
Now anywhere in your app:
// src/other.ts
import config from "zodified-config";
const value = config.get("value");
// value is typed as string
console.log(value);
This is where it starts to feel really good:
- autocomplete for config keys
- compile-time safety for config usage
- fewer “stringly-typed” surprises
A more realistic example
Here's what this looks like for a typical service:
// 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, access becomes boring (in the best way):
import config from "zodified-config";
const port = config.get("port"); // number
const dbUrl = config.get("database.url"); // string
const metricsEnabled = config.get("features.metrics"); // boolean
If someone breaks 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 gives you a lot of safety but configuration is often the escape hatch where untyped values creep back in and quietly undermine it.
Defining a schema for config:
- makes config access typesafe
- improves editor autocomplete and catches key mistakes earlier
Validating config at startup:
- prevents broken deployments
- makes issues obvious in logs and CI/CD
- reduces “works on my machine” surprises
- gives you confidence changing config over time
Tools like zodified-config keep your code and config in sync and it's one of those small changes that makes a big difference to reliability.
If you try it, I'd love to hear how it goes.
Top comments (0)