javascript 91 lines · 3 tabs

Cached Config Singleton in Node.js: Load Once, Share Everywhere

Shared by codesnips Sep 2026
3 tabs
const { z } = require('zod');

const ConfigSchema = z.object({
  env: z.enum(['development', 'test', 'production']).default('development'),
  port: z.coerce.number().int().positive().default(3000),
  database: z.object({
    url: z.string().url(),
    poolSize: z.coerce.number().int().min(1).max(50).default(10),
    ssl: z.coerce.boolean().default(false),
  }),
  features: z.object({
    signups: z.coerce.boolean().default(true),
  }).default({}),
});

module.exports = { ConfigSchema };
3 files · javascript Explain with highlit

A common need in a Node.js service is to read configuration from disk and the environment exactly once at startup, validate it, and then hand the same frozen object to every module that asks for it. This snippet implements that with the module cache: because Node caches the result of require/import per resolved path, a module that computes its config on first load and exports the result naturally becomes a process-wide singleton.

In config.js, the loader merges three layers in precedence order — a JSON file, environment variables, and hard defaults — then runs the merged shape through a schema. The load() function reads the file with readFileSync (synchronous is fine here because it happens once, during boot, before the server accepts traffic), parses it, and overlays envOverrides() so that a PORT or DATABASE_URL in the environment always wins. The parsed config is validated by ConfigSchema from schema.js; schema.parse throws on the first bad field, so a misconfigured deploy fails fast and loudly instead of surfacing an undefined deep inside request handling.

The crucial line is module.exports = deepFreeze(load()). It runs load() a single time at module-evaluation and freezes the object recursively, so consumers cannot accidentally mutate shared state and create spooky action at a distance between modules. Every later require('./config') returns the cached, frozen instance rather than re-reading the file.

schema.js uses zod to describe the expected shape: coercions turn string env vars into numbers, .default() supplies fallbacks, and .url() enforces format. Keeping validation in its own module keeps the loader focused on I/O and merging.

db.js shows a typical consumer. It simply imports config and reads config.database; it never touches process.env or the file itself, which keeps the config surface centralized and testable. The trade-off of the module-cache singleton is that it hides a global and complicates unit testing — swapping config requires jest.resetModules or dependency injection. For that reason the loader is exported as load too, so tests can construct fresh instances with an explicit path. This pattern fits the twelve-factor idea of config living in the environment while giving the rest of the codebase one clean, immutable object to depend on.


Related snips

Share this code

Here's the card — post it anywhere.

Cached Config Singleton in Node.js: Load Once, Share Everywhere — share card
Link copied