首页 / NestJS 入门教程 / 配置管理

NestJS 入门教程

配置管理

本教程共 47 篇 · 第 24 篇 · 更新于 2026-08-09 · 约 11 分钟阅读

NestJS配置管理环境变量ConfigServicedotenvJoi多环境

本节目标:学会用 @nestjs/config 管理应用配置,包括环境变量加载、自定义配置文件、配置验证和命名空间,让应用在不同环境下灵活切换。

你的应用里肯定有不少”会变的东西”:数据库地址、API 密钥、端口号、第三方服务地址……这些东西不能硬编码在代码里。

为什么?因为开发环境、测试环境、生产环境的配置都不一样。你把数据库密码写死在代码里,推到 Git 上,全世界都看到了。

所以我们需要一个正经的配置管理方案。

基本用法:加载 .env 文件

安装

npm install @nestjs/config

@nestjs/config 底层用的是 dotenv 这个包。它做的事很简单:读取 .env 文件里的键值对,塞到 process.env 里。

创建 .env 文件

在项目根目录创建一个 .env 文件:

DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_USER=admin
DATABASE_PASSWORD=secret123
JWT_SECRET=my-super-secret-key
PORT=3000
Warning

.env 文件包含敏感信息,一定要加到 .gitignore 里。永远不要把密钥提交到代码仓库。

注册 ConfigModule

在根模块中导入 ConfigModule

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

@Module({
  imports: [ConfigModule.forRoot()],
})
export class AppModule {}

forRoot() 会自动读取项目根目录下的 .env 文件,把里面的变量合并到 process.env

在代码中读取配置

通过注入 ConfigService 来读取配置值:

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

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

  connect() {
    const host = this.configService.get<string>('DATABASE_HOST');
    const port = this.configService.get<number>('DATABASE_PORT');
    
    console.log(`Connecting to ${host}:${port}`);
  }
}

get() 方法支持泛型,可以指定返回类型。还支持设置默认值:

// 如果 PORT 不存在,返回 3000
const port = this.configService.get<number>('PORT', 3000);
Tip

如果你在很多模块中都要用 ConfigService,可以把 ConfigModule 设为全局模块,这样就不用每个模块都 import 了:

ConfigModule.forRoot({
  isGlobal: true,
})

自定义配置文件

项目大了以后,配置项会越来越多。全塞在一个 .env 文件里不好管理。更好的方式是按功能拆分配置文件。

按功能拆分

// config/database.config.ts
export default () => ({
  database: {
    host: process.env.DATABASE_HOST || 'localhost',
    port: parseInt(process.env.DATABASE_PORT, 10) || 5432,
    username: process.env.DATABASE_USER || 'admin',
    password: process.env.DATABASE_PASSWORD,
    name: process.env.DATABASE_NAME || 'mydb',
  },
});
// config/app.config.ts
export default () => ({
  app: {
    port: parseInt(process.env.PORT, 10) || 3000,
    name: process.env.APP_NAME || 'my-app',
    env: process.env.NODE_ENV || 'development',
  },
});

加载配置文件

import databaseConfig from './config/database.config';
import appConfig from './config/app.config';

@Module({
  imports: [
    ConfigModule.forRoot({
      load: [appConfig, databaseConfig],
    }),
  ],
})
export class AppModule {}

读取嵌套配置

用点号语法读取嵌套的配置值:

// 读取 database.host
const dbHost = this.configService.get<string>('database.host');

// 读取整个 database 对象
const dbConfig = this.configService.get('database');
// dbConfig.host, dbConfig.port, ...

命名空间配置:registerAs

配置文件多了以后,可以用 registerAs 给每个配置文件加一个”命名空间”,读取的时候更清晰。

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

export default registerAs('database', () => ({
  host: process.env.DATABASE_HOST || 'localhost',
  port: parseInt(process.env.DATABASE_PORT, 10) || 5432,
  username: process.env.DATABASE_USER || 'admin',
  password: process.env.DATABASE_PASSWORD,
}));

加载方式和之前一样:

import databaseConfig from './config/database.config';

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

读取时用命名空间做前缀:

// 方式一:用点号语法
const dbHost = this.configService.get<string>('database.host');

// 方式二:直接注入命名空间(推荐,有类型提示)
constructor(
  @Inject(databaseConfig.KEY)
  private dbConfig: ConfigType<typeof databaseConfig>,
) {}

// 使用
this.dbConfig.host  // string
this.dbConfig.port  // number
Tip

方式二的好处是有 TypeScript 类型提示。ConfigType 会自动推导出配置对象的类型,写代码时 IDE 能自动补全。

命名空间配合其他模块使用

命名空间配置可以直接传给其他模块的 forRootAsync,非常方便:

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

@Module({
  imports: [
    TypeOrmModule.forRootAsync(databaseConfig.asProvider()),
  ],
})
export class AppModule {}

asProvider() 会自动把命名空间配置转成 forRootAsync 需要的格式。

配置验证

应用启动时如果关键配置缺失,与其运行到一半才报错,不如直接在启动阶段就拦住。

方式一:用 Joi 验证

安装 Joi:

npm install joi

定义验证规则:

// app.module.ts
import * as Joi from 'joi';

@Module({
  imports: [
    ConfigModule.forRoot({
      validationSchema: Joi.object({
        NODE_ENV: Joi.string()
          .valid('development', 'production', 'test')
          .default('development'),
        PORT: Joi.number().default(3000),
        DATABASE_HOST: Joi.string().required(),
        DATABASE_PORT: Joi.number().default(5432),
        DATABASE_PASSWORD: Joi.string().required(),
        JWT_SECRET: Joi.string().required(),
      }),
    }),
  ],
})
export class AppModule {}

如果验证失败——比如 DATABASE_HOST 没配——应用会直接启动失败,并抛出验证错误。

Note

required() 标记的变量必须存在,否则启动报错。没标记的变量是可选的,配合 default() 可以给默认值。

方式二:用 class-validator 验证

如果你更喜欢装饰器的风格,可以用 class-validator + class-transformer

// env.validation.ts
import { plainToInstance } from 'class-transformer';
import { IsEnum, IsNumber, IsString, Min, Max, validateSync } from 'class-validator';

enum Environment {
  Development = 'development',
  Production = 'production',
  Test = 'test',
}

class EnvironmentVariables {
  @IsEnum(Environment)
  NODE_ENV: Environment;

  @IsNumber()
  @Min(0)
  @Max(65535)
  PORT: number;

  @IsString()
  DATABASE_HOST: string;

  @IsString()
  DATABASE_PASSWORD: string;
}

export function validate(config: Record<string, unknown>) {
  const validatedConfig = plainToInstance(
    EnvironmentVariables,
    config,
    { enableImplicitConversion: true },
  );
  const errors = validateSync(validatedConfig, {
    skipMissingProperties: false,
  });

  if (errors.length > 0) {
    throw new Error(errors.toString());
  }
  return validatedConfig;
}

然后在 ConfigModule 中使用:

import { validate } from './env.validation';

@Module({
  imports: [
    ConfigModule.forRoot({
      validate,
    }),
  ],
})
export class AppModule {}
Tip

两种验证方式选一种就行。Joi 更简洁,class-validator 适合已经在用装饰器风格的项目。

多环境配置

开发、测试、生产环境的配置不一样。常见的做法是为每个环境准备一个 .env 文件:

.env                 # 公共配置(所有环境共享)
.env.development     # 开发环境
.env.production      # 生产环境
.env.test            # 测试环境

根据 NODE_ENV 加载不同文件

@Module({
  imports: [
    ConfigModule.forRoot({
      envFilePath: `.env.${process.env.NODE_ENV || 'development'}`,
    }),
  ],
})
export class AppModule {}

也可以指定多个文件路径,前面的优先级更高:

ConfigModule.forRoot({
  envFilePath: [
    `.env.${process.env.NODE_ENV}.local`,
    `.env.${process.env.NODE_ENV}`,
    '.env',
  ],
})

在 main.ts 中使用配置

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ConfigService } from '@nestjs/config';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  
  const configService = app.get(ConfigService);
  const port = configService.get<number>('PORT', 3000);
  
  await app.listen(port);
  console.log(`Application is running on port ${port}`);
}
bootstrap();

也可以用 Node.js 20+ 的 --env-file 选项,在应用启动前就加载环境变量:

nest start --env-file .env

自定义 Getter 方法

configService.get('SOME_KEY') 每次都要写字符串 key,容易拼错。可以封装一层自定义的 ConfigService:

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

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

  get port(): number {
    return this.configService.get<number>('PORT', 3000);
  }

  get databaseHost(): string {
    return this.configService.get<string>('DATABASE_HOST');
  }

  get databasePort(): number {
    return this.configService.get<number>('DATABASE_PORT', 5432);
  }

  get jwtSecret(): string {
    return this.configService.get<string>('JWT_SECRET');
  }

  get isProduction(): boolean {
    return this.configService.get<string>('NODE_ENV') === 'production';
  }
}

使用的时候就很清晰了:

constructor(private appConfig: AppConfigService) {}

connect() {
  const host = this.appConfig.databaseHost;
  const port = this.appConfig.databasePort;
}
Tip

封装自定义 ConfigService 的好处:类型安全、IDE 自动补全、集中管理配置访问逻辑。项目一大,这个封装就很有价值。

条件加载模块

有时候你想根据环境变量决定是否加载某个模块。比如开发环境加载 MockModule,生产环境加载真实的 ServiceModule。

import { ConditionalModule } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot(),
    ConditionalModule.registerWhen(MockModule, 'USE_MOCK'),
  ],
})
export class AppModule {}

.envUSE_MOCK=true 时加载 MockModule,USE_MOCK=false 或不设置时不加载。

也可以传一个函数做更复杂的判断:

ConditionalModule.registerWhen(
  CacheModule,
  (env: NodeJS.ProcessEnv) => env.NODE_ENV === 'production',
)

变量展开

.env 文件支持变量引用,避免重复写相同的值:

APP_DOMAIN=mywebsite.com
SUPPORT_EMAIL=support@${APP_DOMAIN}
API_URL=https://api.${APP_DOMAIN}

需要开启 expandVariables 选项:

ConfigModule.forRoot({
  expandVariables: true,
})

这样 SUPPORT_EMAIL 会自动解析为 support@mywebsite.com

部分注册

大项目中,不同功能模块可能有自己的配置文件。不用全堆在 AppModule 里,可以在各自的模块中注册:

// database.module.ts
import databaseConfig from './config/database.config';

@Module({
  imports: [ConfigModule.forFeature(databaseConfig)],
})
export class DatabaseModule {}
Note

forFeature() 在模块初始化时执行。如果你在构造函数里访问它加载的配置,可能还没准备好。保险起见,在 onModuleInit 生命周期钩子里访问。

踩坑经验

1. .env 文件中的值不需要引号

# 正确
DATABASE_HOST=localhost

# 也行,但不必要
DATABASE_HOST="localhost"

dotenv 会自动去掉首尾的引号。但如果值里有空格,就需要引号了。

2. process.env 的值全是字符串

.env 文件里写的 PORT=3000,读出来是字符串 "3000",不是数字。需要手动转换:

const port = parseInt(this.configService.get('PORT'), 10);

这就是为什么在自定义配置文件里要用 parseInt() 做类型转换。

3. 运行时环境变量优先级高于 .env 文件

如果你同时设了系统环境变量 PORT=4000.env 文件里 PORT=3000,最终取到的是 4000。系统环境变量优先级更高。

4. 别忘了缓存

ConfigService.get() 每次都会访问 process.env,有一定性能开销。如果频繁读取,开启缓存:

ConfigModule.forRoot({
  cache: true,
})

5. 配置文件不会被 validationSchema 验证

自定义配置文件(通过 load 加载的)不会自动被 validationSchema 验证。如果需要验证自定义配置,在配置文件工厂函数里自己做。

小结

关键知识点回顾:

  • ConfigModule.forRoot() 加载 .env 文件到环境变量
  • ConfigService 注入后读取配置值
  • registerAs 定义命名空间配置,用 asProvider 注册
  • Joi 做配置验证,启动时就能发现配置错误
  • 多环境配置用不同的 .env 文件,通过 envFilePath 指定

下一章我们聊聊数据验证,看看怎么保证请求数据的正确性。