NestJS Logo
NestJS 中文文档
v10.0.0
  • 介绍
  • 快速上手
  • 控制器
  • 提供者
  • 模块
  • 中间件
  • 异常过滤器
  • 管道
  • 守卫
  • 拦截器
  • 自定义装饰器
  • 自定义提供者
  • 异步提供者
  • 动态模块
  • 依赖注入作用域
  • 循环依赖
  • 模块引用
  • 懒加载模块
  • 执行上下文
  • 生命周期事件
  • 发现服务
  • 跨平台无关性
  • 测试
迁移指南
API 参考
官方课程
  1. 文档
  2. 功能扩展
  3. 配置

测试
数据验证

配置

在实际开发中,应用通常会运行在多个不同的环境中,例如本地开发环境、测试环境或生产环境。不同环境下的配置需求往往不同,例如本地环境可能使用专用于本地数据库的凭据,而生产环境则使用另一套更安全的配置。

由于这些配置项在各环境中会有所差异,最佳实践是将这些配置作为环境变量进行管理。

在 Node.js 中,环境变量可以通过全局对象 process.env 访问。虽然可以手动为每个环境设置这些变量,但当你在开发或测试过程中需要频繁修改或模拟配置时,这种做法会变得难以维护。

为了解决这个问题,Node.js 应用通常会使用 .env 文件集中管理配置项。每个 .env 文件由一组键值对组成,用于定义环境变量。通过切换不同的 .env 文件,即可快速切换运行环境。

在 Nest 应用中,推荐的做法是使用配置模块(ConfigModule) 来加载和管理环境变量,并通过配置服务(ConfigService) 进行访问。虽然你也可以手动实现类似功能,但 Nest 官方已经提供了内置的 @nestjs/config 包,大幅简化了这一流程。

本章节将介绍如何使用 @nestjs/config 包进行配置管理。

安装

要开始使用配置功能,首先需要安装官方提供的依赖包:

npm install @nestjs/config
提示

@nestjs/config 底层依赖了 dotenv,用于解析 .env 文件中的环境变量。

注意

@nestjs/config 要求 TypeScript 版本不低于 4.1。

快速上手

安装完成后,你可以通过导入 ConfigModule 快速启用配置功能。通常建议在根模块(AppModule)中使用其静态方法 forRoot() 进行初始化。在此过程中,模块会自动解析并加载 .env 文件中的变量。

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

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

上面的代码会从项目根目录读取 .env 文件,并将其内容与已有的 process.env 变量合并。最终的结果被存储在内部结构中,供 ConfigService 使用。

forRoot() 方法会自动注册 ConfigService,你可以通过它的 get() 方法读取任何环境变量。

由于该模块底层依赖 dotenv,如果同一个变量既存在于 .env 文件中,又已被系统环境变量(例如通过 export 命令)定义,则会优先使用系统环境变量。这一行为符合 dotenv 的解析规则。

例如,下面是一个简单的 .env 文件:

DATABASE_USER=test
DATABASE_PASSWORD=test

提前加载 .env 文件(可选)

如果你希望在应用启动之前(例如在调用 NestFactory.createMicroservice() 之前)就能读取某些环境变量,可以使用 Nest CLI 提供的 --env-file 参数,指定要加载的 .env 文件路径。

提示

--env-file 参数是在 Node.js v20 中引入的,详见 Node.js 官方文档。

示例命令如下:

nest start --env-file .env

自定义 env 文件路径

默认情况下,@nestjs/config 模块会在应用程序的根目录下查找 .env 文件。如果你希望使用自定义路径的配置文件,可以通过 forRoot() 方法的配置对象指定 envFilePath 属性,例如:

ConfigModule.forRoot({
  envFilePath: '.development.env',
})

该属性同样支持传入路径数组,用于指定多个 .env 文件。示例:

ConfigModule.forRoot({
  envFilePath: ['.env.development.local', '.env.development'],
})

当多个文件中存在同名环境变量时,优先使用数组中靠前文件中的值。

禁用 env 文件加载

如果你希望完全跳过 .env 文件的加载,仅从系统环境变量中读取配置(例如通过 shell 预先设置的 export DATABASE_USER=test),可以将 ignoreEnvFile 设置为 true:

ConfigModule.forRoot({
  ignoreEnvFile: true,
})

这样配置后,@nestjs/config 将不会尝试读取任何 .env 文件,仅依赖运行时已有的环境变量。

全局使用配置模块

在 NestJS 中,若多个模块都需要使用 ConfigModule,通常需要分别在每个模块中显式导入它。但为了简化使用,你可以将其声明为一个全局模块:只需在应用的根模块(如 AppModule)中引入一次,之后所有模块中均可直接使用,无需重复导入。

要实现这一点,只需在 forRoot() 方法的配置对象中设置 isGlobal: true:

ConfigModule.forRoot({
  isGlobal: true,
})

开启全局模式后,ConfigService 等提供者将在整个应用中自动可用,有效减少模块之间的重复依赖声明。

自定义配置文件

对于结构更复杂的项目,将配置项按功能模块划分、分别存放在独立的配置文件中,是一种更易维护的做法。NestJS 支持通过「自定义配置文件」方式加载配置内容,只需导出一个返回普通对象的工厂函数即可,该对象可以是任意嵌套结构。

在配置函数中,你可以灵活处理类型转换、设置默认值等逻辑。此时,process.env 已包含来自 .env 文件和系统环境的全部变量,合并规则可参考前文说明。以下是一个基本示例:

config/configuration.ts
export default () => ({
  port: parseInt(process.env.PORT, 10) || 3000,
  database: {
    host: process.env.DATABASE_HOST,
    port: parseInt(process.env.DATABASE_PORT, 10) || 5432,
  },
})

要加载这个配置文件,只需在 ConfigModule.forRoot() 中设置 load 属性:

app.module.ts
import configuration from './config/configuration'

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

load 属性接受一个函数数组,允许一次加载多个配置模块。例如:load: [databaseConfig, authConfig]。

使用自定义配置文件时,也可以管理如 YAML 等自定义格式的文件。以下是一个使用 YAML 格式的配置示例:

加载 YAML 配置文件

NestJS 同样支持加载如 YAML 等自定义格式的配置文件。以下是一个使用 YAML 格式的配置示例:

http:
  host: 'localhost'
  port: 8080

db:
  postgres:
    url: 'localhost'
    port: 5432
    database: 'yaml-db'

  sqlite:
    database: 'sqlite.db'

你可以使用 js-yaml 包来读取并解析 YAML 文件:

npm install js-yaml
npm install -D @types/js-yaml

然后通过 yaml.load() 方法加载配置内容:

config/configuration.ts
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import * as yaml from 'js-yaml'

const YAML_CONFIG_FILENAME = 'config.yaml'

export default () => {
  return yaml.load(
    readFileSync(join(__dirname, YAML_CONFIG_FILENAME), 'utf8')
  ) as Record<string, any>
}
注意

Nest CLI 在编译项目时不会自动复制资源文件(如 YAML)。你需要在 nest-cli.json 的 compilerOptions.assets 字段中显式指定。若你的 config 文件夹位于项目根目录,可配置如下:

"assets": [
  {
    "include": "./config/*.yaml",
    "outDir": "dist/config"
  }
]

详见 Nest CLI 文档。

自定义校验逻辑

需要注意的是:即使你使用了 ConfigModule 的 validationSchema 选项,它也不会自动校验你通过 load 加载的配置文件内容。如果你希望为配置项添加校验逻辑,需要在工厂函数中手动处理。

例如,下面的代码会在端口号不符合预期时抛出错误:

config/configuration.ts
export default () => {
  const config = yaml.load(
    readFileSync(join(__dirname, YAML_CONFIG_FILENAME), 'utf8')
  ) as Record<string, any>

  const port = config.http?.port

  if (typeof port !== 'number' || port < 1024 || port > 49151) {
    throw new Error('HTTP 端口号必须在 1024 到 49151 之间')
  }

  return config
}

这样,当配置不符合预期时,应用会在启动时立即失败,避免潜在隐患。

使用 ConfigService

要在应用中访问配置信息,可以通过注入 ConfigService 实现。和使用其他提供者类似,你需要先在模块中引入 ConfigModule。如果你在 ConfigModule.forRoot() 中将 isGlobal 设置为 true,那么该模块会自动注册为全局模块,无需在每个使用的模块中重复导入。但如果未设置为全局模块,需手动引入,例如:

feature.module.ts
@Module({
  imports: [ConfigModule],
})
export class FeatureModule {}

然后,就可以通过构造函数注入 ConfigService:

import { ConfigService } from '@nestjs/config'

constructor(private configService: ConfigService) {}

在类的方法中,即可使用 get() 方法读取环境变量或自定义配置:

// 读取环境变量
const dbUser = this.configService.get<string>('DATABASE_USER')

// 读取嵌套配置项
const dbHost = this.configService.get<string>('database.host')

如上所示,传入键名即可获取对应的配置值。你也可以通过泛型参数(如 <string>)获得类型提示。对于自定义配置文件中定义的嵌套对象,也可以使用点号语法读取属性值。

如果你希望一次性获取整个嵌套配置对象,可以使用接口来定义类型:

interface DatabaseConfig {
  host: string
  port: number
}

const dbConfig = this.configService.get<DatabaseConfig>('database')

const port = dbConfig.port

此外,get() 方法还支持传入一个默认值,当配置项不存在时会返回该默认值:

// 若 database.host 未定义,则返回 "localhost"
const dbHost = this.configService.get<string>('database.host', 'localhost')

使用泛型增强类型安全

ConfigService 支持两个可选泛型参数。第一个泛型用于约束配置项的结构,从而提高类型安全性。例如:

interface EnvironmentVariables {
  PORT: number
  TIMEOUT: string
}

constructor(private configService: ConfigService<EnvironmentVariables>) {
  const port = this.configService.get('PORT', { infer: true })

  // TypeScript 报错:URL 未在接口中定义
  const url = this.configService.get('URL', { infer: true })
}

当 infer: true 时,get() 方法会根据接口自动推断返回值类型。上例中 PORT 是 number 类型,因此 port 的类型也被自动推断为 number(前提是未启用 strictNullChecks)。

点号语法同样支持类型推断:

constructor(
  private configService: ConfigService<{ database: { host: string } }>
) {
  const dbHost = this.configService.get('database.host', { infer: true })!
  // typeof dbHost === "string"
}
提示

使用非空断言(!)的原因是 get() 方法默认返回 T | undefined,除非你启用了下文所述的第二个泛型参数。

去除返回值中的 undefined

如果你开启了 TypeScript 的 strictNullChecks,可以通过第二个泛型参数来消除返回值的 undefined 类型:

constructor(private configService: ConfigService<{ PORT: number }, true>) {
  const port = this.configService.get('PORT', { infer: true })
  // port 的类型为 number,不再包含 undefined
}

只从配置文件中读取值

如果你希望 ConfigService.get() 只从自定义配置文件中读取值,而不读取 process.env 中的变量,可以在 ConfigModule.forRoot() 中设置 skipProcessEnv: true。

配置的命名空间

ConfigModule 支持加载多个自定义配置文件,适用于复杂或模块化的配置结构。你可以将配置定义为嵌套对象,也可以使用 registerAs() 函数为配置创建命名空间,从而更清晰地组织和访问不同模块的配置项。

以下是一个使用 registerAs() 创建命名空间配置的示例:

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

export default registerAs('database', () => ({
  host: process.env.DATABASE_HOST,
  port: process.env.DATABASE_PORT || 5432,
}))

在 registerAs() 的工厂函数中,你可以使用 process.env 访问环境变量,包括 .env 文件中定义的值和运行时环境中注入的变量。关于环境变量的加载与合并规则,可参考上文。

加载命名空间配置

与普通自定义配置文件类似,你可以将命名空间配置文件通过 load 选项传入 ConfigModule.forRoot():

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

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

此时,配置项会被挂载在以命名空间名称为前缀的路径下(即 registerAs() 中的第一个参数)。

例如,若要获取 database 命名空间中的 host 值,可以使用点号语法:

const dbHost = this.configService.get<string>('database.host')

注入命名空间配置(推荐方式)

相比于字符串键访问,更推荐的方式是直接注入命名空间配置对象,这不仅更清晰,还能获得完整的类型提示:

import { ConfigType } from '@nestjs/config'

constructor(
  @Inject(databaseConfig.KEY)
  private dbConfig: ConfigType<typeof databaseConfig>,
) {
  const host = this.dbConfig.host
}

这种方式充分利用了 TypeScript 的类型推断能力,能有效避免键名拼写错误或类型不匹配的问题,尤其适用于结构复杂的配置对象。

在模块中使用命名空间配置

当你希望将某个命名空间下的配置对象作为参数,传递给其他模块(如数据库模块、第三方模块等)进行初始化时,可以使用命名空间配置对象的 .asProvider() 方法。

该方法会将配置封装为一个标准的异步模块配置对象(即 DynamicModule 中 forRootAsync() 支持的格式),使你能以最小的样板代码完成模块集成。

示例:将 database 命名空间配置传入 TypeOrmModule:

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

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

如上所示,asProvider() 返回的对象可直接传入 forRootAsync() 方法。其本质是一个符合 Nest 异步模块初始化约定的配置结构:

{
  imports: [ConfigModule.forFeature(databaseConfig)],
  useFactory: (config: ConfigType<typeof databaseConfig>) => config,
  inject: [databaseConfig.KEY],
}

工作原理解析

  • imports:通过 ConfigModule.forFeature() 注册命名空间配置;
  • useFactory:定义工厂函数,将配置对象传递给目标模块;
  • inject:指定注入的配置提供者(即命名空间 key)。

这种方式极大提升了模块配置的复用性和结构清晰度,尤其适合在多个模块中复用命名空间配置,避免冗余代码。

缓存环境变量

频繁访问 process.env 可能会成为性能瓶颈。为了提升 ConfigService.get() 方法读取环境变量时的执行效率,可以在调用 ConfigModule.forRoot() 时开启缓存功能:

ConfigModule.forRoot({
  cache: true,
})

启用后,模块会在首次读取时将环境变量缓存起来,后续访问将直接从内存中读取,避免重复解析。

配置的局部注册

在常规用法中,我们通常会在应用的根模块(例如 AppModule)中通过 forRoot() 一次性加载所有配置文件。然而,当项目结构复杂、存在多个特性模块各自维护独立的配置时,集中式注册可能不够灵活。

此时,可以借助 @nestjs/config 提供的局部注册能力,将配置文件按需加载到对应的模块中。通过 ConfigModule.forFeature() 方法,你可以在特性模块中仅注册与当前模块相关的配置:

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

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

在某些场景下,如果你需要访问局部注册的配置项,建议通过 onModuleInit() 生命周期钩子进行,而非在构造函数中直接访问。

这是因为 forFeature() 的注册逻辑会在模块初始化过程中执行,但模块的初始化顺序并不总是可预测的。若在构造函数中尝试访问来自其他模块的配置,可能会因为目标模块尚未完成初始化而导致错误。相比之下,onModuleInit() 会在所有依赖模块完成初始化后才被调用,因此更为可靠。

配置校验

在应用启动阶段,如果缺少必要的环境变量,或者某些变量不符合预期格式,通常应立即抛出异常,防止应用进入不确定状态。 @nestjs/config 模块原生支持两种配置校验方式:

  • 使用内置的 Joi 校验器,定义校验模式(Validation Schema)并验证配置对象。
  • 提供自定义的 validate() 函数,直接对环境变量进行程序化校验。

使用 Joi 进行配置校验

首先,安装 joi:

npm install joi

安装完成后,可以通过 validationSchema 选项,将 Joi 定义的校验模式传入 ConfigModule.forRoot() 方法中。例如:

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

@Module({
  imports: [
    ConfigModule.forRoot({
      validationSchema: Joi.object({
        NODE_ENV: Joi.string()
          .valid('development', 'production', 'test', 'provision')
          .default('development'),
        PORT: Joi.number().port().default(3000),
      }),
    }),
  ],
})
export class AppModule {}

在上例中:

  • NODE_ENV 和 PORT 都定义了默认值。如果 .env 文件或进程环境变量中未提供对应项,则使用默认值。
  • 如果某个变量必须显式提供,可以使用 .required() 方法强制要求该变量存在,否则在启动时将抛出校验错误。

更多 Joi 的校验方法和用法,请参阅 Joi 官方文档。

控制校验行为:validationOptions

默认情况下:

  • 未在 schema 中声明的环境变量(未知变量)是允许的。
  • 所有校验错误会被汇总报告,而不是在遇到第一个错误时立即中止。

你可以通过 validationOptions 选项修改上述行为。该选项接受 Joi 的标准配置项,例如:

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

@Module({
  imports: [
    ConfigModule.forRoot({
      validationSchema: Joi.object({
        NODE_ENV: Joi.string()
          .valid('development', 'production', 'test', 'provision')
          .default('development'),
        PORT: Joi.number().port().default(3000),
      }),
      validationOptions: {
        allowUnknown: false, // 禁止未声明的变量
        abortEarly: true, // 遇到第一个错误即中止
      },
    }),
  ],
})
export class AppModule {}

需要特别注意的是: 一旦你传入了 validationOptions,未显式指定的属性将使用 Joi 的默认值,而不是 @nestjs/config 的默认设置。

例如:@nestjs/config 默认 allowUnknown 为 true,但 Joi 默认是 false。如果你未在 validationOptions 中手动设置 allowUnknown,则它会被自动重置为 Joi 的默认值 false,可能导致某些变量无法通过校验。

因此,强烈建议你在自定义 validationOptions 时,显式声明所有关键选项,以避免意外行为。

可选配置:关闭预定义变量校验

如果你希望跳过对「预定义环境变量」的校验(例如在启动命令中直接设置的变量),可以通过以下方式禁用:

ConfigModule.forRoot({
  validatePredefined: false,
})

所谓「预定义环境变量」,是指在导入 ConfigModule 之前就已经存在于 process.env 中的变量,例如通过命令行启动应用时设置的变量:

PORT=3000 node main.js

此时 PORT 就是一个预定义变量。

自定义环境变量校验函数

你可以通过实现一个同步的 validate 函数,来自定义环境变量的校验逻辑。该函数会接收一个包含所有环境变量(包括 .env 文件和进程环境变量)的原始对象,并返回一个经过校验和必要转换的配置对象。

注意

若校验函数抛出错误,应用将在启动阶段终止,无法正常运行。

以下示例展示了如何结合 class-validator 与 class-transformer 这两个库来实现这一功能。主要步骤如下:

  1. 定义一个带有验证装饰器的类,用于描述配置项结构及其校验规则。
  2. 实现一个 validate 函数,利用 plainToInstance 将普通对象转换为类实例,并使用 validateSync 进行同步校验。
env.validation.ts
import { plainToInstance } from 'class-transformer'
import { IsEnum, IsNumber, Max, Min, validateSync } from 'class-validator'

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

class EnvironmentVariables {
  @IsEnum(Environment)
  NODE_ENV: Environment

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

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 时传入该函数即可:

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

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

自定义 getter 函数

ConfigService 提供了通用的 get() 方法,用于通过键名读取配置项。但在实际开发中,如果某些配置项频繁被访问,或者你希望以更语义化的方式表达配置含义,可以在服务中封装成自定义的 getter 属性,使代码更清晰、自然。

下面是一个示例,展示如何封装布尔型配置项 AUTH_ENABLED:

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

  get isAuthEnabled(): boolean {
    return this.configService.get('AUTH_ENABLED') === 'true'
  }
}

这样,你可以在其他服务中以更简洁的方式使用配置值:

app.service.ts
@Injectable()
export class AppService {
  constructor(private readonly apiConfigService: ApiConfigService) {
    if (this.apiConfigService.isAuthEnabled) {
      // 已启用身份验证逻辑
    }
  }
}
建议

使用 getter 属性封装常用或具有业务语义的配置项,有助于集中管理配置逻辑,提升代码的表达清晰度。

环境变量加载钩子

在某些场景下,模块的初始化过程可能依赖环境变量,而这些变量又来自 .env 文件。为了确保在访问 process.env 之前,.env 文件已成功加载,ConfigModule 提供了一个静态钩子:ConfigModule.envVariablesLoaded。

你可以在异步逻辑中等待该钩子完成,确保配置环境已就绪:

export async function getStorageModule() {
  await ConfigModule.envVariablesLoaded

  return process.env.STORAGE === 'S3' ? S3StorageModule : DefaultStorageModule
}

通过使用 envVariablesLoaded,可以避免在 .env 尚未加载时就访问 process.env,从而避免潜在的初始化错误或配置缺失问题。

提示

仅在 ConfigModule 尚未完全初始化、但你又需要读取环境变量的模块加载逻辑中使用该钩子。若已通过 ConfigService 注入配置,一般无需使用此钩子。