在实际开发中,应用通常会运行在多个不同的环境中,例如本地开发环境、测试环境或生产环境。不同环境下的配置需求往往不同,例如本地环境可能使用专用于本地数据库的凭据,而生产环境则使用另一套更安全的配置。
由于这些配置项在各环境中会有所差异,最佳实践是将这些配置作为环境变量进行管理。
在 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 文件中的变量。
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默认情况下,@nestjs/config 模块会在应用程序的根目录下查找 .env 文件。如果你希望使用自定义路径的配置文件,可以通过 forRoot() 方法的配置对象指定 envFilePath 属性,例如:
ConfigModule.forRoot({
envFilePath: '.development.env',
})该属性同样支持传入路径数组,用于指定多个 .env 文件。示例:
ConfigModule.forRoot({
envFilePath: ['.env.development.local', '.env.development'],
})当多个文件中存在同名环境变量时,优先使用数组中靠前文件中的值。
如果你希望完全跳过 .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 文件和系统环境的全部变量,合并规则可参考前文说明。以下是一个基本示例:
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 属性:
import configuration from './config/configuration'
@Module({
imports: [
ConfigModule.forRoot({
load: [configuration],
}),
],
})
export class AppModule {}load 属性接受一个函数数组,允许一次加载多个配置模块。例如:load: [databaseConfig, authConfig]。
使用自定义配置文件时,也可以管理如 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() 方法加载配置内容:
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 加载的配置文件内容。如果你希望为配置项添加校验逻辑,需要在工厂函数中手动处理。
例如,下面的代码会在端口号不符合预期时抛出错误:
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,那么该模块会自动注册为全局模块,无需在每个使用的模块中重复导入。但如果未设置为全局模块,需手动引入,例如:
@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() 创建命名空间配置的示例:
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: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 模块原生支持两种配置校验方式:
validate() 函数,直接对环境变量进行程序化校验。首先,安装 joi:
npm install joi安装完成后,可以通过 validationSchema 选项,将 Joi 定义的校验模式传入 ConfigModule.forRoot() 方法中。例如:
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默认情况下:
你可以通过 validationOptions 选项修改上述行为。该选项接受 Joi 的标准配置项,例如:
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 这两个库来实现这一功能。主要步骤如下:
validate 函数,利用 plainToInstance 将普通对象转换为类实例,并使用 validateSync 进行同步校验。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 时传入该函数即可:
import { Module } from '@nestjs/common'
import { ConfigModule } from '@nestjs/config'
import { validate } from './env.validation'
@Module({
imports: [
ConfigModule.forRoot({
validate,
}),
],
})
export class AppModule {}ConfigService 提供了通用的 get() 方法,用于通过键名读取配置项。但在实际开发中,如果某些配置项频繁被访问,或者你希望以更语义化的方式表达配置含义,可以在服务中封装成自定义的 getter 属性,使代码更清晰、自然。
下面是一个示例,展示如何封装布尔型配置项 AUTH_ENABLED:
@Injectable()
export class ApiConfigService {
constructor(private readonly configService: ConfigService) {}
get isAuthEnabled(): boolean {
return this.configService.get('AUTH_ENABLED') === 'true'
}
}这样,你可以在其他服务中以更简洁的方式使用配置值:
@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 注入配置,一般无需使用此钩子。