手把手教你如何優(yōu)雅的書寫NestJS服務配置
前言
開發(fā)服務端應用,配置肯定是少不了的。端口號、數(shù)據(jù)庫賬號、Redis 密碼、各種業(yè)務白名單……這些東西如果硬編碼在代碼里,不僅不安全,每次切環(huán)境還得改代碼,簡直是災難。
配置的來源五花八門:簡單的可以直接讀環(huán)境變量(env),稍微復雜點的用 JSON、YAML 配置文件,遇到敏感信息或者微服務架構,可能還得去 Nacos、Apollo 這樣的遠端配置中心拉取。
在 NestJS 里,寫配置的方式有很多。如果團隊沒個統(tǒng)一的規(guī)范,大家各寫各的,雖然代碼也能跑,但后期維護起來絕對會讓人抓狂。這幾年再使用 NestJS 過程中踩了不少坑,摸索出了一套比較舒服的配置管理姿勢,今天就來系統(tǒng)地梳理一遍。
1. 刀耕火種:直接硬剛 process.env
最原始、最暴力的寫法,就是直接在代碼里讀 process.env:
// 這種代碼散落在項目的各個角落,后期維護極其痛苦 const port = process.env.PORT || 3000; const dbHost = process.env.DATABASE_HOST;
這種寫法最大的問題是毫無約束。字段名字全靠腦子記,類型全都是 string | undefined,還得自己手動轉數(shù)字、轉布爾值。哪天要是改個環(huán)境變量的名字,你得全局搜索一遍才能確認是不是都改全了。所以,這只能算是個起點,正經(jīng)項目千萬別這么搞。
2. 官方標配:@nestjs/config 模塊
為了解決配置問題,NestJS 官方提供了一個 @nestjs/config 包。它的底層其實就是我們熟悉的 dotenv,這也是目前處理配置的標準入口。
先裝個包(注意:它要求 TypeScript 4.1 及以上版本):
npm i --save @nestjs/config
2.1 怎么用起來?
最基礎的用法,就是在根模塊 AppModule 里引入 ConfigModule,調一下 forRoot()。它會自動去項目根目錄找 .env 文件并解析:
// app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true, // 強烈建議設為全局,這樣其他模塊就不用反復 import 了
}),
],
})
export class AppModule {}?? 踩坑預警:當系統(tǒng)的環(huán)境變量和
.env文件里出現(xiàn)了同名的 key 時,系統(tǒng)環(huán)境變量的優(yōu)先級更高,.env里的值會被無情覆蓋。這是 dotenv 的默認規(guī)則,在線上部署排查問題時一定要記住這一點。
設置了 isGlobal: true 后,你在任何 Service 里都可以直接注入 ConfigService 來拿配置了。
2.2 應對多環(huán)境
真實項目肯定不止一個環(huán)境,起碼有開發(fā)(dev)、測試(staging)、生產(chǎn)(prod)。我們可以傳個數(shù)組給 envFilePath,讓它按需加載。通常配合 NODE_ENV 動態(tài)決定加載哪個文件:
ConfigModule.forRoot({
envFilePath: [
`.env.${process.env.NODE_ENV}.local`, // 優(yōu)先級最高:本地覆蓋文件
`.env.${process.env.NODE_ENV}`, // 當前環(huán)境配置
'.env', // 兜底默認配置
],
isGlobal: true,
});?? 踩坑預警:數(shù)組里越靠前的文件優(yōu)先級越高。如果幾個文件里有相同的 key,它只會認第一個找到的值,后面的文件里重復的 key 會被靜默忽略。
3. 進階:把"散裝字符串"變成結構化對象
只用 .env 有個很煩人的點:讀出來的全是一層扁平的字符串。如果配置一多,找起來費勁,而且到處都要寫 parseInt。
NestJS 允許我們寫配置工廠函數(shù),把這些零散的環(huán)境變量組裝成有結構、有類型、有默認值的對象。
// config/database.config.ts
export default () => ({
port: parseInt(process.env.PORT, 10) || 3000,
database: {
host: process.env.DATABASE_HOST || 'localhost',
port: parseInt(process.env.DATABASE_PORT, 10) || 5432,
user: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
},
});然后在 AppModule 里通過 load 屬性把這個工廠函數(shù)塞進去:
import databaseConfig from './config/database.config';
ConfigModule.forRoot({
isGlobal: true,
load: [databaseConfig], // 這里是個數(shù)組,可以塞多個配置進去,互不干擾
});用的時候,就可以用點號(.)來讀取嵌套屬性了:
const dbHost = this.configService.get<string>('database.host');
const dbPort = this.configService.get<number>('database.port');這比到處散落的 process.env.DATABASE_HOST 好多了,至少做到了集中管理。但這種寫法還有個問題:字符串路徑 'database.host' 沒有類型提示,寫錯了只能靠運行時暴露。下面這個方案才是真正的終極解法。
4. 終極利器:命名空間(Namespace)
registerAs() 函數(shù)是 @nestjs/config 里最值得推薦的功能。它不只是給配置起個名字,更重要的是它返回的對象帶有 .KEY 屬性,可以直接用于依賴注入——這意味著你可以注入整個配置對象,而不是一個個字符串地去 get()。
4.1 劃分命名空間
把不同業(yè)務的配置拆到各自的文件里:
// 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,
name: process.env.DATABASE_NAME,
}));// config/redis.config.ts
import { registerAs } from '@nestjs/config';
export default registerAs('redis', () => ({
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT, 10) || 6379,
}));然后在根模塊統(tǒng)一加載:
ConfigModule.forRoot({
isGlobal: true,
load: [databaseConfig, redisConfig],
});4.2 享受強類型的快感
用了命名空間后,可以直接注入整個配置對象,TypeScript 類型推導完整,IDE 有完整提示,字段名寫錯了編譯階段就報錯:
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 dbConfig: ConfigType<typeof databaseConfig>,
) {}
getConnection() {
// 敲下 this.dbConfig. 的時候,IDE 會完整提示 host, port, name
return `${this.dbConfig.host}:${this.dbConfig.port}/${this.dbConfig.name}`;
}
}?? 小貼士:
ConfigType<typeof databaseConfig>是官方提供的工具類型,它能自動反推工廠函數(shù)的返回值結構,省得你再去手寫一遍 Interface。
4.3 絲滑對接第三方模塊
命名空間配置還有個殺手锏:.asProvider() 方法。
平時配置 TypeORM 或 Redis 這類第三方模塊時,往往要寫一長串 useFactory、inject、imports。用了 .asProvider() 后,這些樣板代碼全部省掉——它的本質是把依賴聲明和工廠函數(shù)打包成了標準的 forRootAsync 入?yún)?,NestJS 會據(jù)此建立正確的模塊初始化順序:
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true, load: [databaseConfig] }),
// 等價于手寫 imports/inject/useFactory 的完整異步配置
TypeOrmModule.forRootAsync(databaseConfig.asProvider()),
],
})
export class AppModule {}關于為什么這能保證初始化順序,以及手寫 useFactory 時要注意什么,我們在第 7.2 節(jié)會展開說。
5. 換個口味:YAML、JSON 怎么搞?
.env 用起來最順手,但它有個天然的短板:所有值讀出來都是字符串,端口、超時時間這些數(shù)字都得自己手動 parseInt,布爾值也要寫 === 'true' 來判斷,一不小心就出錯。
相比之下,YAML 和 JSON 本身就支持數(shù)字、布爾等原生類型,寫配置的時候是什么類型讀出來還是什么類型,完全不需要轉換。而且層級結構更清晰,改起來更直觀,后期維護成本也低得多。如果項目的配置項比較多,主動選擇 YAML 或 JSON 來管理是個很合理的決定。
@nestjs/config 的 load 機制非常靈活,只要你的工廠函數(shù)最終返回一個普通對象,它才不管數(shù)據(jù)是從哪來的。
5.1 玩轉 YAML
先裝解析庫:npm i js-yaml 和 npm i -D @types/js-yaml。
// config/yaml.config.ts
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import * as yaml from 'js-yaml';
export default () => {
return yaml.load(
readFileSync(join(__dirname, 'config.yaml'), 'utf8'),
) as Record<string, any>;
};?? 踩坑預警:Nest CLI 默認打包時不會拷貝非 TS 文件。如果你用了 YAML,一定要去
nest-cli.json里配置assets,不然打完包跑起來絕對報找不到文件的錯。{ "compilerOptions": { "assets": [{ "include": "../config/*.yaml", "outDir": "./dist/config" }] } }
5.2 遠端配置中心(如 Nacos、Apollo)
如果你們用了配置中心,最常見的有兩種接入方式:
方式一:CD 流程在容器啟動前下發(fā)配置文件,把配置寫到本地磁盤,應用啟動時當普通本地文件讀,和 5.1 的 YAML 方式一模一樣,沒有額外復雜度。
方式二:應用啟動時主動拉取,把工廠函數(shù)寫成異步的,在 ConfigModule 初始化期間完成拉?。?/p>
import { registerAs } from '@nestjs/config';
export default registerAs('secret', async () => {
// 去遠端拉配置,拉不到就拋異常,阻止應用啟動
const data = await fetchFromConfigCenter('/api/config/secret');
return { apiKey: data.apiKey, jwtSecret: data.jwtSecret };
});兩種方式的選擇取決于你們的部署流程,不存在好壞之分。
6. 防患未然:配置校驗
沒有校驗的配置就像是一顆定時炸彈。萬一線上環(huán)境少配了一個數(shù)據(jù)庫密碼,應用可能照樣啟動,直到用戶發(fā)起請求才原地爆炸。
我們必須讓應用在啟動階段就把缺少的配置暴露出來。NestJS 給了兩套方案:
6.1 簡單粗暴的 Joi 校驗
import * as Joi from 'joi';
ConfigModule.forRoot({
validationSchema: Joi.object({
NODE_ENV: Joi.string().valid('dev', 'prod').default('dev'),
PORT: Joi.number().default(3000),
DATABASE_HOST: Joi.string().required(), // 必填項,沒有就不準啟動!
}),
validationOptions: {
allowUnknown: true, // 允許出現(xiàn)沒在 schema 里定義的變量
abortEarly: false, // 別一遇到錯就停,把所有錯一塊報出來
},
});?? 踩坑預警:如果你傳了
validationOptions,那些沒寫的選項會回退到 Joi 的默認值,而不是@nestjs/config的默認值!比如allowUnknown,Joi 默認是false,但 Nest 默認是true。為了不被坑,建議把這倆屬性老老實實寫清楚。
6.2 面向對象的 class-validator
如果你更喜歡用類和裝飾器,也可以自己寫個校驗函數(shù):
import { plainToInstance } from 'class-transformer';
import { IsNumber, IsString, validateSync } from 'class-validator';
class EnvVariables {
@IsNumber() PORT: number;
@IsString() DATABASE_HOST: string;
}
export function validate(config: Record<string, unknown>) {
const validated = plainToInstance(EnvVariables, config, { enableImplicitConversion: true });
const errors = validateSync(validated, { skipMissingProperties: false });
if (errors.length > 0) throw new Error(errors.toString());
return validated;
}// app.module.ts
ConfigModule.forRoot({ validate });兩種校驗方式效果相同,Joi 更簡潔,class-validator 在項目里已經(jīng)有 DTO 校驗體系時風格更統(tǒng)一,按喜好選就行。
7. 特殊場景
7.1 局部注冊(forFeature)的生命周期坑
如果你的項目比較大,不想把所有配置都塞在 AppModule 里,可以在各自的業(yè)務模塊里按需加載:
@Module({
imports: [ConfigModule.forFeature(databaseConfig)],
})
export class DatabaseModule {}?? 踩坑預警:用
forFeature注冊的配置,千萬別在構造函數(shù)(constructor)里去讀!因為模塊初始化的順序是不確定的,這時候配置可能還沒加載完。正確的做法是用屬性注入,然后在onModuleInit生命周期鉤子里讀取。
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
import { ConfigType } from '@nestjs/config';
import databaseConfig from './database.config';
@Injectable()
export class DatabaseService implements OnModuleInit {
// 屬性注入,不在構造函數(shù)里觸碰配置
@Inject(databaseConfig.KEY)
private readonly dbConfig: ConfigType<typeof databaseConfig>;
onModuleInit() {
// ? 到這里所有依賴模塊都已初始化完畢,安全讀取
console.log('db host:', this.dbConfig.host);
}
// ? 下面這種寫法會在某些場景下讀到 undefined
// constructor(
// @Inject(databaseConfig.KEY)
// private readonly dbConfig: ConfigType<typeof databaseConfig>,
// ) {
// console.log(this.dbConfig.host); // 危險!配置可能還沒就緒
// }
}7.2 配置之間的依賴鏈問題
這是一個很容易被忽略但實際開發(fā)中會真實踩到的坑。
以 TypeORM 為例,完整的啟動依賴鏈是這樣的:
ConfigModule 加載 .env / 配置文件
↓
databaseConfig 工廠函數(shù)讀取環(huán)境變量,生成配置對象
↓
TypeOrmModule 拿到配置,建立數(shù)據(jù)庫連接池
↓
各業(yè)務 Service / Repository 可以正常使用
↓
應用啟動完成,開始接受請求這條鏈上的順序必須是確定的。如果配置還沒就緒,TypeOrmModule 就開始初始化,輕則連接參數(shù)是 undefined,重則應用直接起不來。
第 4.3 節(jié)提到的 .asProvider() 能自動保證這個順序,因為它展開后等價于:
{
imports: [ConfigModule.forFeature(databaseConfig)], // 聲明:我依賴這個配置
useFactory: (config: ConfigType<typeof databaseConfig>) => config,
inject: [databaseConfig.KEY],
}imports 字段就是向 NestJS 聲明依賴關系的地方。框架看到這個聲明,就會等 databaseConfig 就緒后再初始化 TypeOrmModule,順序由框架保證,不需要你操心。
如果不用 .asProvider(),自己手寫 useFactory,一定要記得寫 imports,不然就是在賭運氣:
TypeOrmModule.forRootAsync({
imports: [ConfigModule], // ← 必須寫!告訴框架:等 ConfigModule 好了再來初始化我
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
type: 'mysql',
host: configService.get<string>('database.host'),
port: configService.get<number>('database.port'),
// ...
}),
}),還有一個更細的場景:兩個配置文件之間能互相依賴嗎?比如 databaseConfig 想引用 appConfig 里的某個值。答案是不行,@nestjs/config 不支持配置工廠之間的注入。正確的做法是所有工廠函數(shù)都平級地從 process.env 取原始值,誰需要哪個環(huán)境變量就自己讀,不要試圖跨工廠函數(shù)共享:
// ? 錯誤:試圖在配置工廠里引用另一個配置對象
export default registerAs('database', () => {
const appConfig = someHowGetAppConfig(); // 根本不存在這種 API
return { host: appConfig.defaultHost };
});
// ? 正確:直接從 process.env 取,各配置工廠平級獨立
export default registerAs('database', () => ({
host: process.env.APP_DEFAULT_HOST || 'localhost',
}));7.3 在 main.ts 里怎么拿配置?
像監(jiān)聽端口、CORS 域名這種配置,在 main.ts 里就要用到,這時候可以通過 app.get() 拿到 ConfigService 實例:
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);
}8. 總結:到底該怎么選?
寫了這么多,咱們來拉個表對比一下:
| 姿勢 | 類型安全 | 結構化 | 評價 |
|---|---|---|---|
直接硬剛 process.env | ? 純盲寫 | ? 扁平 | 適合寫完就扔的臨時腳本 |
ConfigModule + .env | 弱(全靠泛型強轉) | ? 扁平 | 適合小型項目快速起步 |
| 自定義工廠函數(shù) | ?(需手寫類型) | ? 嵌套對象 | 中規(guī)中矩,能處理默認值和類型轉換 |
| registerAs() 命名空間 | ??(全自動推導) | ? 按業(yè)務隔離 | 強烈推薦!中大型項目的標準答案 |
?? 個人最推薦的實戰(zhàn)組合: registerAs() 命名空間 + isGlobal: true + 啟動時 Joi 強校驗。
這套組合的核心邏輯是:配置在哪定義,就在哪描述它的結構;業(yè)務代碼只管用,根本不需要關心底層到底是 .env 還是 YAML。 這樣哪怕以后要把配置遷移到云端,業(yè)務代碼也一行都不用改,這才叫真正的優(yōu)雅。
到此這篇關于如何優(yōu)雅的書寫NestJS服務配置的文章就介紹到這了,更多相關NestJS服務配置內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
基于NodeJS開發(fā)釘釘回調接口實現(xiàn)AES-CBC加解密
這篇文章主要介紹了基于NodeJS開發(fā)釘釘回調接口 實現(xiàn)AES-CBC加解密,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-08-08
node.js express框架實現(xiàn)文件上傳與下載功能實例詳解
這篇文章主要介紹了node.js express框架實現(xiàn)文件上傳與下載功能,結合具體實例形式詳細分析了node.js express框架針對文件上傳與下載的前后臺相關實現(xiàn)技巧,需要的朋友可以參考下2019-10-10
一文詳解NodeJS和Javascript之間有什么區(qū)別
在前端和后端開發(fā)的技術棧中,JavaScript?和?Node.js?經(jīng)常是新手和經(jīng)驗豐富的開發(fā)者討論的熱門話題,這篇文章主要介紹了NodeJS和Javascript之間有什么區(qū)別的相關資料,文中通過代碼介紹的非常詳細,需要的朋友可以參考下2025-11-11
Node學習筆記:Node.js安裝及環(huán)境配置 史詩級詳細版【含測試與鏡像說明】
這篇文章主要介紹了Node學習筆記之Node.js安裝及環(huán)境配置方法,詳細分析了node.js的基本安裝、配置、環(huán)境變量設置、以及環(huán)境測試與鏡像使用說明,需要的朋友可以參考下2023-05-05
Node安裝教程&環(huán)境變量配置方式(Window11)
這篇文章詳細介紹了如何在Windows 11上安裝和配置Node.js,包括下載、安裝、驗證安裝、創(chuàng)建目錄、配置環(huán)境變量、下載驗證以及更換淘寶鏡像等步驟2026-03-03

