Configuration
Configuration in nestrs is a typed struct, one file per concern, mapped
explicitly to NESTRS_<NAMESPACE>__<KEY> environment variables. The crate
is hand-rolled (no figment at runtime): you write from_env, the framework
loads it once at boot, validates it, and registers Arc<MyConfig> into
the container. Consumers inject it like any other provider.
The whole pipeline reads from environment variables and a small .env
cascade the framework merges before any from_env runs. No TOML loader,
no YAML, no remote source baked in — but a ConfigSource
trait lets you ship one when you need Vault, K8s, or AWS Parameter Store.
Install
Section titled “Install”cargo add nest-rs-configA typed config — by example
Section titled “A typed config — by example”The common case is three short blocks: a #[config] struct, a derived
Validate, and a from_env that maps one line per variable.
use nest_rs_config::{config, Config, ConfigService};use validator::Validate;
#[config(namespace = "posts")]#[derive(Clone, Validate)]pub struct PostsConfig { #[validate(range(min = 1, max = 500))] pub page_size: u32, pub cache_enabled: bool,}
impl Config for PostsConfig { fn from_env(env: &ConfigService) -> nest_rs_config::Result<Self> { Ok(Self { page_size: env.parse("PAGE_SIZE")?.unwrap_or(50), cache_enabled: env.flag("CACHE_ENABLED", false)?, }) }}Three pieces do the heavy lifting:
#[config(namespace = "posts")]binds the struct to theNESTRS_POSTS__*scheme. The macro wires theNamespacedtrait — no manual constant to write.impl Config { fn from_env(env: &ConfigService) }is the hand-written contract: one line per variable. A reviewer reading this file knows exactly which env vars exist, with which defaults, and what happens when one is malformed.#[derive(Validate)]plus#[validate(...)]attributes run after the load. A failure aborts boot naming the field. A rule the derive cannot express is the one case for a hand-writtenimpl Validate— see the Advanced section at the end of this page.
env.parse("PAGE_SIZE")? reads + parses into the field’s type; unset
returns None, set-but-unparseable returns Err naming the
variable — boot-fatal, no silent fallback. env.flag accepts the
usual boolean spellings.
The dual-path rule
Section titled “The dual-path rule”Every field a module’s config exposes must be reachable two ways:
- The pinned struct, passed to
Module::for_root(MyConfig { ... })— programmatic, type-checked, comes from code. - The
NESTRS_<NS>__<KEY>environment variable, mapped explicitly infrom_env.
That is the framework-wide rule for every nest-rs-* module. An
env-only field can’t be set from a config file; a code-only field can’t
be overridden in deployment. Both, always.
ConfigService API
Section titled “ConfigService API”The reader handed to from_env is a thin wrapper around the namespace
prefix. Five methods, all sync.
| Method | Reads |
|---|---|
env.get("KEY") → Option<String> | NESTRS_<NS>__KEY as a raw string |
env.parse::<T>("KEY")? → Option<T> | Same, parsed via FromStr |
env.flag("KEY", default)? → bool | 1/true/yes/on and their negatives (case-insensitive) |
env.list("KEY") → Vec<String> | Comma-separated, trimmed, empties dropped |
env.var_name("KEY") → String | The full env-var name (for error messages) |
Empty strings count as unset, so FOO= in a .env file does not blank
an in-code default.
Wire it in
Section titled “Wire it in”The wiring is two module imports. The feature owns its config; the root
module imports ConfigModule::for_root() once.
use nest_rs_config::ConfigModule;use super::config::PostsConfig;use super::service::PostsService;
#[module( imports = [ConfigModule::for_feature::<PostsConfig>()], providers = [PostsService],)]pub struct PostsModule;use nest_rs_config::ConfigModule;
#[module( imports = [ ConfigModule::for_root(), PostsHttpModule, ],)]pub struct AppModule;ConfigModule::for_root()goes first in the root module. It merges the.envcascade once (real env vars always win) and registersArc<Environment>so every laterConfig::loadsees the merged environment.ConfigModule::for_feature::<C>()queues a factory in the boot’s factory phase. The factory callsC::load()(=from_env+validate); on failure the boot aborts with the variable named.
Inject it
Section titled “Inject it”A consumer injects Arc<PostsConfig> like any provider — the type is
the token.
#[injectable]pub struct PostsService { #[inject] config: Arc<PostsConfig>,}
impl PostsService { pub async fn list(&self) -> Result<Vec<Post>, ServiceError> { let page = self.config.page_size; // ... }}Run it
Section titled “Run it”A bad value fails the boot before any port opens:
$ NESTRS_POSTS__PAGE_SIZE=lots nestrs run dev apiError: invalid value for NESTRS_POSTS__PAGE_SIZE: invalid digit found in stringA violated #[validate] rule aborts the same way after the load,
naming the field.
Why hand-written from_env
Section titled “Why hand-written from_env”Auto-deserializing from env vars sounds convenient, but the contract
becomes implicit (which vars exist? what are their types?). With
from_env written explicitly:
- The full env contract of a feature is one impl block, in one file.
- A missing variable falls back to a code default, not silently to
Default::default(). - Unparseable values fail loudly, naming the variable.
- A reviewer reads the contract by reading ten lines, not by chasing serde attributes.
Advanced
Section titled “Advanced”Hand-writing from_env (for custom validation, or a field carrying a structured payload, or a rule spanning the whole struct) is covered on Advanced configuration with the real IssuerConfig as the worked example.
Going further
Section titled “Going further”Reference
Section titled “Reference”crates/features/src/oauth/config.rs— the full real example.crates/nest-rs-config/—#[config],Config,ConfigService,ConfigModule, the.envcascade.