Skip to content
NestJS ns config 4 min read

Namespaced & Typed Config

As an application grows, a flat configuration object quickly becomes unwieldy: you lose track of which keys belong to the database, which to authentication, and which to third-party integrations. NestJS solves this with configuration namespaces created via registerAs, which group related settings under a single key and give you a fully typed factory you can inject anywhere. Combined with the ConfigType helper, you get compile-time safety and editor autocompletion instead of stringly-typed configService.get('jwt.secret') lookups that fail silently when a key is misspelled.

Creating a namespace with registerAs

registerAs takes a namespace name and a factory function that returns a plain configuration object. The factory runs lazily when the configuration is loaded, so it is the perfect place to read and coerce environment variables. Keep one namespace per concern — typically one file each for database, jwt, redis, and so on.

// config/database.config.ts
import { registerAs } from '@nestjs/config';

export default registerAs('database', () => ({
  host: process.env.DB_HOST ?? 'localhost',
  port: parseInt(process.env.DB_PORT ?? '5432', 10),
  username: process.env.DB_USER ?? 'postgres',
  password: process.env.DB_PASSWORD ?? '',
  name: process.env.DB_NAME ?? 'app',
}));
// config/jwt.config.ts
import { registerAs } from '@nestjs/config';

export default registerAs('jwt', () => ({
  secret: process.env.JWT_SECRET ?? 'dev-only-secret',
  expiresIn: process.env.JWT_EXPIRES_IN ?? '15m',
  refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN ?? '7d',
}));

The default export from registerAs is special: it is both a configuration factory and carries a KEY property and a callable token used for typed injection. That dual nature is what powers everything below.

Loading namespaces into the module

Register the factories through the load array of ConfigModule.forRoot. Each namespace is merged into the global configuration tree under its own key, so the database settings live at database and JWT settings at jwt.

// app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import databaseConfig from './config/database.config';
import jwtConfig from './config/jwt.config';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      cache: true,
      load: [databaseConfig, jwtConfig],
    }),
  ],
})
export class AppModule {}

Deriving a typed shape with ConfigType

Rather than hand-writing an interface for each namespace, use the ConfigType utility type. It infers the return type of the factory, so the type and the runtime values can never drift apart.

import { ConfigType } from '@nestjs/config';
import databaseConfig from './config/database.config';

type DatabaseConfig = ConfigType<typeof databaseConfig>;
// => { host: string; port: number; username: string; password: string; name: string }

Injecting a namespace into a provider

Because the factory exposes a KEY token, you inject the namespaced object directly with @Inject(config.KEY). The injected value is fully typed via ConfigType, so config.port is a number and the compiler rejects typos like config.prt.

// database.service.ts
import { Inject, Injectable } from '@nestjs/common';
import { ConfigType } from '@nestjs/config';
import databaseConfig from './config/database.config';

@Injectable()
export class DatabaseService {
  constructor(
    @Inject(databaseConfig.KEY)
    private readonly config: ConfigType<typeof databaseConfig>,
  ) {}

  describe(): string {
    return `Connecting to ${this.config.host}:${this.config.port}/${this.config.name}`;
  }
}
// app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { DatabaseService } from './database.service';

@Controller()
export class AppController {
  constructor(private readonly db: DatabaseService) {}

  @Get('db')
  describeDb(): string {
    return this.db.describe();
  }
}

Output:

$ curl http://localhost:3000/db
Connecting to localhost:5432/app

Tip: Inject the namespace token (databaseConfig.KEY), not the whole ConfigService. This narrows a provider’s dependencies to exactly the settings it needs, which keeps unit tests trivial — you simply pass a plain object matching the ConfigType shape.

Accessing namespaces through ConfigService

You can still read a namespace via ConfigService when injecting the token is inconvenient. Use the infer: true option so the return type is inferred from the namespace rather than widened to unknown.

import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import jwtConfig from './config/jwt.config';

@Injectable()
export class TokenService {
  constructor(private readonly configService: ConfigService) {}

  ttl(): string {
    // Typed as the jwt namespace object
    const jwt = this.configService.get(jwtConfig.KEY, { infer: true })!;
    return jwt.expiresIn;
  }
}
Access patternHow you read itType safety
@Inject(config.KEY)Constructor injection of the namespaceFull, via ConfigType
configService.get(config.KEY, { infer: true })Lookup by tokenFull, inferred
configService.get('jwt.secret')Dotted string keyNone — returns unknown/string

Partial configuration for feature modules

A feature module rarely needs every namespace. Register only the namespaces it owns with ConfigModule.forFeature, which makes that namespace available locally without polluting the global tree. This is ideal for self-contained modules published as libraries.

// auth/auth.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import jwtConfig from '../config/jwt.config';
import { TokenService } from './token.service';

@Module({
  imports: [ConfigModule.forFeature(jwtConfig)],
  providers: [TokenService],
  exports: [TokenService],
})
export class AuthModule {}

Warning: forFeature registers the namespace for injection but does not re-parse .env files or run global validation. Keep forRoot (with validationSchema) at the application root and use forFeature purely to expose a namespace token to a specific module.

Best practices

  • Keep one registerAs namespace per concern (database, jwt, redis) in its own file under a config/ directory.
  • Always derive types with ConfigType<typeof factory> instead of maintaining a hand-written interface that can drift.
  • Inject the namespace token (config.KEY) into providers so dependencies are explicit and easy to mock in tests.
  • Coerce and default values inside the factory (parseInt, ??) so injected config is already the correct runtime type.
  • Use { infer: true } whenever you read through ConfigService to preserve type information.
  • Reserve forFeature for module-scoped namespaces, and keep validation centralized in the root forRoot call.
Last updated June 14, 2026
Was this helpful?