nestrs’ configuration layer lives inside the nestrs::config module and
is the in-tree analogue of @nestjs/config. Sources merge in declaration
order (later wins); null / empty values delete the previous key; YAML,
TOML, and JSON files are all converted to a JSON tree before the typed
decode runs, so the typed-access path only ever sees one shape.
Define a typed, namespaced config
ConfigNamespace declares the prefix used both for env-var matching
(NESTRS_DB__HOST) and for the dotted-key view inside config files
(db.host). One config struct per concern — db, redis, auth,
feature_flags. The #[derive(Validate)] runs on boot, so an invalid
config panics at startup rather than mid-request.
Register and load
ConfigModule::for_root reads from the process environment (with a
dotenvy cascade if a .env file is present at startup), parses each
entry against its namespace, runs validation, and exports a
ConfigService into the DI container. Boot-time panic on invalid
config — match the @nestjs/config derive’s panic-on-load behavior.
Read from a provider
get_by_key::<T>("db") looks up the namespace, decodes the merged
overlay, and returns the typed value. The string "db" matches
DatabaseConfig::NAMESPACE, so the call site reads like a logical
section rather than an env-var path.
Layer file sources
config/app.json— committed defaults, parsed at compile-time shape.config/local.json— developer overrides.OptionalFilesilently skips a missing path (matches@nestjs/config’signoreEnvFilesemantics).Env { prefix: None }— the process environment, always last so deployments can override without rebuilding.
null in any source deletes the previous value — the
typed decode then either errors or fills with the struct’s default.
Raw dotted-key view
When the typed path is too narrow — feature flags with dynamic keys, templating, debugging — pull the merged overlay directly:snapshot() is the resolved view with namespace re-keying (db.host →
NESTRS_DB__HOST), the same shape env-var lookups see. For the
pre-rekey view, read service.raw (also a HashMap<String, String>).
File formats
FileFormat is {Json, Toml, Yaml}. Use FileFormat::from_path to
pick from the file extension when you don’t want to be explicit:
"true" becomes Bool(true), "8080" becomes Number(8080),
"" / "null" becomes Null. Numeric / bool decoding only succeeds
when the entire string round-trips, so get_by_key::<u16>("db.port")
never silently truncates.
Hot reload
With theconfig-hot-reload feature on the nestrs crate, attach a
ConfigWatcher after building the initial ConfigService:
notify to subscribe to fs events on every
File / OptionalFile source, debounces bursts, and rebuilds the
service on a blocking thread (the decode doesn’t need the tokio
runtime). Subsequent reads from the injected ConfigService see the
new values. Env and Inline sources are not watched (they aren’t
filesystem-resident), so a config drift via env var won’t trigger a
reload — by design.
When to use what
- Typed
Config::register<T>()— every production config. Decode- validate at boot, get compile errors on rename, and
get_by_keyreads like a logical section.
- validate at boot, get compile errors on rename, and
ConfigService::snapshot()— debugging, dynamic feature flags, or logging the resolved overlay at startup. Read-only.ConfigWatcher— dev loops where config files change frequently and a restart is friction. Don’t ship it behind a prod feature flag — the watcher adds fs syscall overhead per reload, and production reload is usually a deploy event, not a runtime concern.
See also
- Concepts: modules — for how
importwires a dynamic module into a feature module. - Concepts: providers —
ConfigServiceis a regular provider; it participates in DI like any other. @nestjs/config— the upstream analogue, for comparison.