DEV Community

Cover image for NestJS vs CabloyJS Env and Config Architecture: From Environment Variables to Instance-Aware Configuration
Uncle Pushui
Uncle Pushui

Posted on

NestJS vs CabloyJS Env and Config Architecture: From Environment Variables to Instance-Aware Configuration

The hard part is not reading .env. It is deciding where a value comes from, what can override it, when configuration becomes available, and which configuration a request should finally see.

A single .env file is usually enough for a small service: a port, a database URL, and a Redis password. Once deployment shapes, test isolation, module reuse, customer-specific behavior, or multi-tenancy enter the picture, configuration stops being a list of variables. It becomes part of the runtime architecture.

This article compares NestJS @nestjs/config 4.0.4 with the current Cabloy Basic Vona backend. The short version is: NestJS provides composable application-configuration tools; CabloyJS/Vona organizes mode, flavor, module configuration, and instance-effective configuration into one framework-level chain.

The short conclusion

Dimension NestJS @nestjs/config CabloyJS / Vona
Primary role A composable application-configuration toolkit Part of a layered runtime and configuration model
Environment selection The app declares .env paths and loading policy The CLI selects mode/flavor, then resolves cascading env/config layers
Runtime validation Joi / custom validate entry points Strong config shapes; no equivalent centralized validation phase in the core startup path
Module ownership registerAs(), load, forFeature() Module config resource plus project config.modules[...] overrides
Request-level variation The app composes tenant/request configuration itself ctx.config exposes merged, instance-effective configuration

Both systems can consume environment variables. The difference is the question they prioritize: NestJS focuses on how an application composes configuration capabilities; Vona focuses on whether configuration belongs to the application, a module, or an active instance.

NestJS: a composable model centered on process.env

NestJS typically begins with ConfigModule.forRoot(): it reads env files, combines them with existing process.env, and provides ConfigService. An application can independently choose Joi schemas, a synchronous custom validate(), configuration factories, namespaces, caching, and variable expansion.

ConfigModule.forRoot({
  isGlobal: true,
  envFilePath: ['.env.production.local', '.env.production'],
  validationSchema: Joi.object({
    PORT: Joi.number().port().required(),
  }),
});
Enter fullscreen mode Exit fullscreen mode

Two precedence rules must be kept separate. With multiple envFilePath entries, earlier files take priority. Values already injected into process.env before startup override matching values from those files by default. This works well for Docker, Kubernetes, and CI/CD: an image can supply defaults while platform secrets or command-line injection retain deployment priority. If production should trust only externally injected variables, use ignoreEnvFile: true.

That env-loading order is not the same as the lookup order of ConfigService.get(). In 4.x, get() checks internal custom configuration first, then validated environment configuration, then falls back to process.env and the caller's default. In other words, “process environment overrides .env” does not mean it always overrides configuration created with registerAs().

NestJS's major strength is startup-time validation. validationSchema and validate() can reject an invalid port, a missing database endpoint, or an unsafe production flag while the application starts, and they can turn string input into numbers or booleans at that boundary. The generic in ConfigService.get<number>('PORT') does not perform runtime conversion by itself.

registerAs() can also give database, authentication, or messaging configuration an explicit namespace and support typed injection through ConfigType<typeof config>:

export const databaseConfig = registerAs('database', () => ({
  host: process.env.DATABASE_HOST,
  port: Number(process.env.DATABASE_PORT ?? 5432),
}));

constructor(
  @Inject(databaseConfig.KEY)
  private readonly database: ConfigType<typeof databaseConfig>,
) {}
Enter fullscreen mode Exit fullscreen mode

Together with ConfigModule.forFeature(databaseConfig), configuration can be registered with a feature module instead of accumulating in the root module. Partial registration has a lifecycle boundary, however: reading another module's configuration too early in a constructor can happen before that target module initializes. In that case, defer the read to a safer lifecycle boundary such as onModuleInit(). Also, cache: true primarily caches ConfigService reads of process.env; it is not a universal configuration or remote-configuration cache.

Vona: configuration as a runtime chain

Vona does not start with a standalone configuration package. It starts with runtime dimensions established by the CLI:

  • META_MODE, such as dev, test, and prod;
  • META_FLAVOR, such as normal, docker, and ci, or a project-defined flavor.

The CLI derives NODE_ENV from mode and normalizes SERVER_WORKERS: production defaults to the CPU count, while non-production defaults to 1. Mode and flavor are therefore framework runtime inputs determined before application startup, not strings scattered through business code.

Both env and project config cascade

For prod + docker, env files can follow a chain like this:

.env → .env.prod → .env.prod.docker
     → .env.local / .env.prod.local / .env.prod.docker.local
Enter fullscreen mode Exit fullscreen mode

.local is the highest-priority local override layer. When Vona produces the final runtime env object, pre-existing process.env values can still override matching keys. Project config follows the same model: config.ts → mode → flavor → local. The selected config functions may run asynchronously and are then deep-merged in deterministic order, translating env input into structured server, logger, Redis, and database configuration.

This gives local development, Docker builds, CI, and externally injected deployment values one explainable precedence model rather than several unrelated startup scripts.

Module defaults and project overrides have explicit ownership

A Vona module can define reusable defaults in src/config/config.ts; the project overrides them through config.modules['module-name']:

module default config → current project's config.modules[module-name]
Enter fullscreen mode Exit fullscreen mode

The current module reads configuration through this.scope.config; another module's configuration is available through this.$scope.<module>.config. Configuration is therefore a module resource alongside service, model, entity, and locale resources. Module authors own reusable defaults, while the project owns deployment- and product-specific policy. In a system that reuses suites and modules, this is easier to audit than an informal rule that every key comes from one global service.

ctx.config is the distinguishing layer

app.config is the global application baseline. Once a request enters an instance context, ctx.config is that instance's effective configuration:

app.config
  → static instance configuration
  → configuration persisted on the instance record
  → ctx.config
Enter fullscreen mode Exit fullscreen mode

This does not mean NestJS cannot support multi-tenancy. A NestJS application can build equivalent business capabilities with request-scoped providers, middleware, and its own tenant configuration service. The difference is architectural ownership: a typical NestJS project designs tenant resolution, configuration merging, and datasource routing itself; Vona lets instance resolution, effective configuration, startup, and datasource behavior share one runtime model.

When multi-instance deployments, instance isolation, or customer-level variation are recurring requirements, that shared model reduces the drift caused by every module interpreting the tenant independently. If a service will always have one application-wide configuration, the additional flavor and instance vocabulary can also be unnecessary overhead.

The key distinction: type safety is not runtime validation

Vona configuration functions and module metadata infer configuration shapes. scope.config therefore provides strong type guidance and helps prevent misspelled fields or invalid cross-module access. But the current central env/config startup path does not provide a single runtime validation stage equivalent to NestJS validationSchema or validate().

A Vona project should still treat ports, databases, external credentials, and production safety switches as startup contracts, and validate them explicitly in project config or a dedicated startup-validation boundary. Conversely, a NestJS project should not turn every domain setting into an unowned ConfigService.get('...') string lookup merely because it uses Joi.

A more durable layering model is:

  1. env is external, string-based deployment input;
  2. a validation boundary rejects invalid input and performs conversions;
  3. project config organizes application-level runtime structure;
  4. module config owns defaults and reusable capability;
  5. behavior that differs per instance has an explicit request-level effective configuration.

NestJS provides mature tools for point 2. Vona provides fuller framework conventions for points 3, 4, and 5. CabloyJS's value is not replacing a dotenv package; it is turning configuration into runtime architecture that can be understood and audited as one chain.

How to choose

Choose NestJS @nestjs/config when the service is primarily a conventional application with few deployment shapes, and the team values explicit env validation and freedom to compose factories, namespaces, and providers.

Choose—or benefit from—CabloyJS/Vona when a project repeatedly needs mode/flavor-driven builds and deployments, reusable suite/module defaults with project overrides, and instance-effective configuration. It asks the team to learn more runtime vocabulary, but prevents them from reinventing configuration rules for every module, tenant, and deployment shape.

Sources and implementation references

NestJS

CabloyJS / Vona

Top comments (0)