Node.js Configuration Management with Convict, Env-Var, or Zod: A Complete Guide
The goldbergyoni/nodebestpractices repository recommends a layered configuration strategy that combines hierarchical config files, environment variables, and runtime validation using libraries like Convict, env-var, or Zod to ensure type safety and fail-fast behavior.
Effective Node.js configuration management requires more than simple JSON files to handle the complexity of production environments. According to the goldbergyoni/nodebestpractices repository, applications need a robust stack that separates secrets from source code and validates settings before the server starts. This guide explores how to implement the repository's recommended layered approach using Convict, env-var, or Zod to achieve flawless, environment-aware configuration.
The Layered Configuration Strategy
As documented in sections/projectstructre/configguide.md, the repository advocates for a hierarchical configuration system that merges multiple sources in a specific order:
- Base configuration files (e.g.,
config/default.json) establish sensible defaults grouped by feature domain such asserver.portordb.host. - Environment-specific overrides (e.g.,
config/production.json) customize settings for particular deployment targets without modifying defaults. - Environment variables (e.g.,
process.env.DB_HOST) take precedence and inject secrets securely at runtime. - Runtime validation via schema libraries ensures the merged configuration is complete and type-safe before the application boots.
This strategy ensures that secrets stay out of source control while maintaining fail-fast validation that catches missing or malformed values immediately on startup rather than during runtime errors.
Choosing the Right Configuration Library
The repository highlights three primary libraries for implementing this strategy, each suited to different architectural needs.
Convict for Hierarchical File-Based Configuration
Convict excels in large applications requiring complex, hierarchical configuration structures. It provides declarative schemas with built-in environment variable mapping, format conversion, and custom validation functions.
Key features include default value support, automatic loading of environment-specific JSON files, and the ability to mark fields as sensitive to prevent secrets from appearing in logs. Convict validates the entire configuration tree once during initialization, throwing descriptive errors for any violated constraints.
Env-Var for Pure Environment Variable Access
Env-var offers a minimalist approach focused exclusively on type-safe access to process.env. Unlike Convict, it requires no configuration files, making it ideal for containerized microservices and serverless functions where environment variables serve as the single source of truth.
The library provides chainable API methods like .required(), .default(), and type converters (.asPortNumber(), .asString()) that enforce constraints at runtime with clear error messages.
Zod for TypeScript-First Validation
Zod provides pure TypeScript schemas with powerful composition capabilities, making it the preferred choice for TypeScript-heavy codebases already using the library for request validation. Unlike Convict or env-var, Zod validates arbitrary JavaScript objects rather than binding directly to environment variables or files.
This flexibility allows teams to load configuration from any source (files, environment variables, or remote services), merge the results into a plain object, and then validate against a Zod schema that simultaneously enforces runtime constraints and infers static TypeScript types.
Implementation Examples
The following examples demonstrate how to structure configuration using each library according to the nodebestpractices guidelines.
Using Convict
// config.js
import convict from 'convict';
import path from 'path';
// Load base + environment‑specific files
const config = convict({
env: {
doc: 'Application environment',
format: ['production', 'development', 'test'],
default: 'development',
env: 'NODE_ENV',
},
server: {
port: {
doc: 'Port to bind',
format: 'port',
default: 3000,
env: 'PORT',
},
},
db: {
host: {
doc: 'Database host',
format: String,
default: 'localhost',
env: 'DB_HOST',
},
password: {
doc: 'Database password',
format: String,
default: '',
sensitive: true,
env: 'DB_PASSWORD',
},
},
});
// Load optional JSON files (default + env specific)
config.loadFile([
path.join(__dirname, 'config', 'default.json'),
path.join(__dirname, 'config', `${config.get('env')}.json`),
]);
// Perform validation – throws if any required value is missing/invalid
config.validate({ allowed: 'strict' });
export default config;
Using Env-Var
// config.js
import env from 'env-var';
export const SERVER_PORT = env.get('PORT').required().asPortNumber();
export const DB_HOST = env.get('DB_HOST').default('localhost').asString();
export const DB_PASSWORD = env.get('DB_PASSWORD').required().asString(); // secret
Using Zod
// config.ts
import { z } from 'zod';
import fs from 'fs';
import path from 'path';
// Load JSON files (fallback to empty object if missing)
const base = JSON.parse(fs.readFileSync(path.resolve('config/default.json'), 'utf-8'));
const envConfig = JSON.parse(
fs.readFileSync(path.resolve(`config/${process.env.NODE_ENV || 'development'}.json`), 'utf-8')
);
// Merge with env‑vars (env vars win)
const rawConfig = {
...base,
...envConfig,
server: {
port: process.env.PORT ?? base.server?.port,
},
db: {
host: process.env.DB_HOST ?? base.db?.host,
password: process.env.DB_PASSWORD ?? base.db?.password,
},
};
// Zod schema – validates and infers TypeScript types
const ConfigSchema = z.object({
env: z.enum(['production', 'development', 'test']).default('development'),
server: z.object({
port: z.number().int().min(1).max(65535).default(3000),
}),
db: z.object({
host: z.string().nonempty(),
password: z.string().min(1),
}),
});
export const config = ConfigSchema.parse(rawConfig);
export type Config = typeof config; // exported type for the rest of the codebase
Security and Secrets Management
As emphasized in sections/security/secretmanagement.md, configuration management is inseparable from secrets management. The repository mandates that database passwords, API keys, and cryptographic secrets must never be hard-coded in source files committed to version control.
Instead, inject sensitive values via environment variables or secure vaults, then mark them as sensitive in your validation schema (Convict's sensitive: true flag or Zod's validation constraints). This approach ensures that even if configuration objects are logged during debugging, secrets remain masked or excluded from output.
Summary
- Node.js configuration management requires a layered approach combining files, environment variables, and runtime validation to prevent production failures.
- The
nodebestpracticesrepository insections/projectstructre/configguide.mdspecifically recommends Convict for hierarchical file-based configs, env-var for pure environment variable access, and Zod for TypeScript-first validation. - Fail-fast validation ensures applications throw immediately on startup if required configuration is missing or invalid, rather than failing mysteriously during request handling.
- Secrets must never be committed to source control; use environment variables or secure vaults and mark sensitive fields accordingly in your chosen validation library.
- Choose Convict for complex, multi-file configurations, env-var for lightweight containerized services, and Zod when TypeScript type inference and schema composition are priorities.
Frequently Asked Questions
What is the primary advantage of using Convict over simple JSON configuration files?
Convict provides declarative schema validation, automatic environment variable mapping, and hierarchical file loading that prevents "it works in dev but crashes in prod" scenarios. As implemented in the goldbergyoni/nodebestpractices examples, it validates the entire configuration tree on startup and throws descriptive errors for missing values or type mismatches.
Can I use Zod for configuration if my application is not written in TypeScript?
Yes, Zod functions perfectly in JavaScript projects, though you lose the static type inference benefits. The library validates JavaScript objects at runtime against defined schemas, making it suitable for any Node.js application requiring robust configuration validation, regardless of whether you use TypeScript.
How does env-var differ from dotenv for managing environment variables?
While dotenv loads variables from .env files into process.env, env-var provides type-safe access and validation of those variables through a chainable API. According to the nodebestpractices source code analysis, env-var focuses on enforcing constraints (required fields, type conversion, defaults) after variables are loaded, whereas dotenv only handles the loading mechanism.
Where should I store sensitive configuration values like database passwords?
Sensitive values should never be stored in code repositories or plain configuration files. As documented in sections/security/secretmanagement.md, inject secrets via environment variables or secure vaults, then reference them in your validation schema. Use features like Convict's sensitive: true flag to ensure these values are automatically redacted from logs and error messages.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →