confure
v0.1.2
Published
Zod-native, type-safe configuration for Node services — one descriptor yields a validated, defaulted, env-bound value with secret redaction and fail-fast production guards.
Maintainers
Readme
confure
Zod-native, type-safe configuration for Node services. Declare each setting once — with its env var, default, docs, and secrecy — and get back a validated, defaulted, env-bound value object with end-to-end inferred types, secret redaction, and fail-fast production guards.
confure turns one declarative descriptor into three things: a Zod validation schema, a defaults object, and the environment-variable bindings. Layers merge in a fixed precedence (defaults → per-env files → .env.local → env vars) and collapse into one .parse() at boot — coercion included. Because the value type is inferred from the descriptor, config.get('jwt.secret') is checked — both the path and the return type — at compile time. No hand-written interfaces, no unchecked string reads.
Install
npm install confure
# or: yarn add confureRequires Node >=18. zod is a runtime dependency. @nestjs/common/@nestjs/core are optional — only the confure/nest subpath imports them.
Usage
Declare the shape once, then read it typed:
import { cf, withConfig } from 'confure';
const config = withConfig(
{
env: cf.enum({
env: 'NODE_ENV',
values: ['development', 'test', 'staging', 'production'] as const,
default: 'development',
doc: 'Runtime environment',
}),
service: {
name: cf.string({ env: 'SERVICE_NAME', default: 'app' }),
port: cf.port({ env: 'PORT', default: 3000 }),
},
postgres: {
url: cf.url({ env: 'PG_URL', default: 'postgres://postgres:postgres@localhost:5432/app' }),
},
jwt: {
secret: cf.string({ env: 'JWT_SECRET', default: '', sensitive: true, requiredInProd: true }),
},
},
{ dir: import.meta.dirname }, // enables per-env JSON files + .env.local; optional
);
config.get('service.port'); // number — compile error if the path is wrong
config.getAll(); // the whole validated, typed object
config.toSafeJSON(); // same object, `jwt.secret` → "[redacted]"Precedence (low → high): defaults → {env}.json → {env}.local.json → env vars → parse. Coercion lives in each builder's schema, so env strings become numbers/booleans/arrays in the single fail-fast pass.
Sections compose and override cleanly:
import { withConfig } from 'confure';
import { postgres, service } from 'confure/presets';
const config = withConfig({
...service({ name: { default: 'billing' } }),
...postgres({ url: { env: 'PG_URL_2' } }),
});NestJS users opt into DI without pulling framework code into the core:
import { createConfigModule, InjectConfig } from 'confure/nest';
import type { Config } from 'confure';
@Module({ imports: [createConfigModule(config)] })
export class AppModule {}
@Injectable()
class Billing {
constructor(@InjectConfig() private readonly config: Config<AppConfig>) {}
}What's in here
Core (confure)
cf— field builders:string,number,boolean,port,enum,array,url,base64, andobject(plus plain nesting). Each takes{ env?, default?, doc?, sensitive?, requiredInProd? }and returns aConfigField<T>whose value type flows into the inferred config object.withConfig(descriptor, opts?)— returns a typedConfig<T>:get(path)— compile-time-checked dot path + return type;getAll()/raw()— the whole validated object;toSafeJSON()— deep clone with everysensitiveleaf masked.
ConfigOptions—dir,env,files,dotenvLocal,strict,redactValue,onInsecureProd, and arefinehook (a consumersuperRefinefor conditional validation, e.g.driver === 's3' ⇒ s3.bucket required).- Engine —
buildZodSchema(strict-by-defaultz.strictObject),buildDefaults,buildEnvBindings: the three structures derived from one descriptor. - Files —
resolveConfigDir(dist-vs-src fallback),loadConfigFiles({env}.jsonthen{env}.local.json). .env.localloader —loadLocalDevEnv: once-per-process, non-clobbering, test-runner-aware, walks up to a configurable root marker (defaultpackage.json/nx.json).- Sections —
mergeSection+SectionOverrides<S>for building reusable, override-friendly config sections.
Guards (production hygiene)
- Redaction —
toSafeJSON()maskssensitiveleaves in any serialization, soJSON.stringify(config.toSafeJSON())is log-safe. requiredInProd— refuses to boot when a flagged field is empty or still equals its insecure declared default inproduction(throwby default,warnopt-in). Fail-fast on the classic "shipped the dev secret" bug.- Strict-by-default — unknown keys in descriptors/files throw at boot; opt out with
strict: false.
Optional subpaths
confure/nest—createConfigModule(config)(a@Globalmodule), theCONFURE_CONFIGtoken, andInjectConfig().confure/presets— genericservice,postgres,redis,logger,observabilitysections with placeholder defaults (no service topology; override the values for your deployment).
Design
See DESIGN.md for the goals, the descriptor engine, and the rationale behind each decision.
MIT © confure contributors
