Ứng dụng thường chạy ở nhiều môi trường (environment) khác nhau. Tùy vào môi trường, các cấu hình sử dụng sẽ khác nhau. Ví dụ, môi trường local thường dùng thông tin đăng nhập database riêng, chỉ hợp lệ với DB instance local. Môi trường production sẽ dùng một bộ thông tin đăng nhập DB khác. Vì các biến cấu hình thay đổi theo môi trường, best practice là lưu các biến cấu hình trong environment.
Các biến môi trường được định nghĩa từ bên ngoài sẽ hiển thị bên trong Node.js thông qua global process.env. Ta có thể thử giải quyết vấn đề nhiều môi trường bằng cách set biến môi trường riêng cho từng môi trường. Cách này nhanh chóng trở nên cồng kềnh, đặc biệt ở môi trường development và testing, nơi các giá trị này cần được mock và/hoặc thay đổi dễ dàng.
Trong ứng dụng Node.js, cách phổ biến là dùng file .env chứa các cặp key-value, mỗi key đại diện cho một giá trị cụ thể, để biểu diễn từng môi trường. Chạy app ở các môi trường khác nhau lúc này chỉ đơn giản là đổi qua file .env tương ứng.
Một cách tiếp cận tốt để áp dụng kỹ thuật này trong Nest là tạo một ConfigModule expose ra ConfigService, chịu trách nhiệm load file .env phù hợp. Bạn có thể tự viết module như vậy, nhưng để tiện lợi, Nest cung cấp sẵn package @nestjs/config. Chương này sẽ nói về package đó.
Ghi chú người dịch Đây thực chất là bản dịch của các khái niệm quen thuộc với Laravel (.env + config() helper) hay Go (viper/envconfig). Điểm khác biệt là ở Nest, ConfigService không phải biến global tiện dụng như config() của Laravel — nó là một provider bình thường, phải inject qua constructor như mọi service khác. Điều này giúp bạn test dễ hơn (mock ConfigService khi viết unit test) nhưng cũng có nghĩa bạn không thể đọc config ở nơi không có DI container (ví dụ file cấu hình decorator chạy ở module-load-time).
Cài đặt
Để bắt đầu, trước tiên ta cài dependency cần thiết.
$ npm i --save @nestjs/config
Gợi ý Package @nestjs/config sử dụng dotenv bên trong.
Cảnh báo @nestjs/config yêu cầu TypeScript 4.1 trở lên.
Bắt đầu (Getting started)
Sau khi cài đặt xong, ta có thể import ConfigModule. Thông thường ta sẽ import nó vào AppModule gốc và điều khiển hành vi của nó bằng static method .forRoot(). Ở bước này, các cặp key/value của biến môi trường sẽ được parse và resolve. Ở phần sau, ta sẽ xem một vài cách để truy cập class ConfigService của ConfigModule trong các module tính năng khác.
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [ConfigModule.forRoot()],
})
export class AppModule {}
Đoạn code trên sẽ load và parse file .env từ vị trí mặc định (thư mục gốc của project), merge các cặp key/value từ file .env với các biến môi trường đã gán vào process.env, rồi lưu kết quả vào một cấu trúc riêng tư mà bạn có thể truy cập thông qua ConfigService. Method forRoot() đăng ký provider ConfigService, cung cấp method get() để đọc các biến cấu hình đã parse/merge này. Vì @nestjs/config dựa trên dotenv, nó sử dụng quy tắc của package đó để giải quyết xung đột tên biến môi trường. Khi một key tồn tại cả trong biến môi trường của runtime (ví dụ qua OS shell export như export DATABASE_USER=test) lẫn trong file .env, biến môi trường của runtime sẽ được ưu tiên.
Một file .env mẫu trông như sau:
DATABASE_USER=test
DATABASE_PASSWORD=test
Nếu bạn cần một số biến env sẵn sàng ngay cả trước khi ConfigModule được load và ứng dụng Nest được bootstrap (ví dụ để truyền cấu hình microservice cho method NestFactory#createMicroservice), bạn có thể dùng option --env-file của Nest CLI. Option này cho phép bạn chỉ định đường dẫn tới file .env cần được load trước khi ứng dụng khởi động. Hỗ trợ flag --env-file được giới thiệu từ Node v20, xem tài liệu để biết thêm chi tiết.
$ nest start --env-file .env
Đường dẫn file env tùy chỉnh (Custom env file path)
Mặc định, package sẽ tìm file .env trong thư mục gốc của ứng dụng. Để chỉ định đường dẫn khác cho file .env, set property envFilePath của object options (tùy chọn) truyền vào forRoot(), như sau:
ConfigModule.forRoot({
envFilePath: '.development.env',
});
Bạn cũng có thể chỉ định nhiều đường dẫn file .env như sau:
ConfigModule.forRoot({
envFilePath: ['.env.development.local', '.env.development'],
});
Nếu một biến xuất hiện trong nhiều file, file đầu tiên sẽ được ưu tiên.
Tắt việc load biến env (Disable env variables loading)
Nếu bạn không muốn load file .env, mà chỉ muốn truy cập biến môi trường từ runtime environment (như OS shell export export DATABASE_USER=test), set property ignoreEnvFile của object options thành true, như sau:
ConfigModule.forRoot({
ignoreEnvFile: true,
});
Sử dụng module như global module
Khi muốn dùng ConfigModule trong các module khác, bạn cần import nó (như với mọi module Nest thông thường). Hoặc, bạn có thể khai báo nó là global module bằng cách set property isGlobal của object options thành true, như dưới đây. Khi đó, bạn không cần import ConfigModule vào các module khác một khi nó đã được load ở module gốc (ví dụ AppModule).
ConfigModule.forRoot({
isGlobal: true,
});
File cấu hình tùy chỉnh (Custom configuration files)
Với các project phức tạp hơn, bạn có thể dùng file cấu hình tùy chỉnh để trả về các object cấu hình lồng nhau (nested). Cách này cho phép bạn nhóm các cấu hình liên quan theo chức năng (ví dụ: cấu hình liên quan đến database), và lưu các cấu hình liên quan trong các file riêng để quản lý độc lập.
Một file cấu hình tùy chỉnh export một factory function trả về một object cấu hình. Object cấu hình có thể là bất kỳ plain JavaScript object lồng nhau tùy ý nào. Object process.env sẽ chứa các cặp key/value biến môi trường đã được resolve đầy đủ (với file .env và các biến định nghĩa từ bên ngoài đã được resolve và merge như mô tả ở trên). Vì bạn kiểm soát object cấu hình trả về, bạn có thể thêm bất kỳ logic nào cần thiết để ép kiểu giá trị, set giá trị mặc định, v.v. Ví dụ:
export default () => ({
port: parseInt(process.env.PORT, 10) || 3000,
database: {
host: process.env.DATABASE_HOST,
port: parseInt(process.env.DATABASE_PORT, 10) || 5432
}
});
Ta load file này bằng property load của object options truyền vào method ConfigModule.forRoot():
import configuration from './config/configuration.js';
@Module({
imports: [
ConfigModule.forRoot({
load: [configuration],
}),
],
})
export class AppModule {}
Lưu ý Giá trị gán cho property load là một mảng, cho phép bạn load nhiều file cấu hình (ví dụ load: [databaseConfig, authConfig])
Với file cấu hình tùy chỉnh, ta cũng có thể quản lý các file tùy chỉnh khác như file YAML. Đây là ví dụ về cấu hình dùng định dạng YAML:
http:
host: 'localhost'
port: 8080
db:
postgres:
url: 'localhost'
port: 5432
database: 'yaml-db'
sqlite:
database: 'sqlite.db'
Để đọc và parse file YAML, ta có thể tận dụng package js-yaml.
$ npm i js-yaml
$ npm i -D @types/js-yaml
Sau khi cài đặt package xong, ta dùng function yaml#load để load file YAML vừa tạo ở trên.
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import yaml from 'js-yaml';
const YAML_CONFIG_FILENAME = 'config.yaml';
export default () => {
return yaml.load(
readFileSync(join(import.meta.dirname, YAML_CONFIG_FILENAME), 'utf8'),
) as Record<string, any>;
};
Cảnh báo Nest CLI không tự động copy các “asset” (file không phải TS) vào thư mục dist trong quá trình build. Để đảm bảo file YAML của bạn được copy, bạn phải khai báo điều này trong object compilerOptions#assets ở file nest-cli.json. Ví dụ, nếu thư mục config nằm cùng cấp với thư mục src, thêm compilerOptions#assets với giá trị "assets": [{"include": "../config/*.yaml", "outDir": "./dist/config"}]. Đọc thêm tại đây.
Một lưu ý nhanh - các file cấu hình không được tự động validate, kể cả khi bạn dùng option validationSchema trong ConfigModule của NestJS. Nếu cần validate hoặc muốn áp dụng bất kỳ transformation nào, bạn phải tự xử lý bên trong factory function, nơi bạn có toàn quyền kiểm soát object cấu hình. Điều này cho phép bạn triển khai bất kỳ logic validate tùy chỉnh nào theo nhu cầu.
Ví dụ, nếu bạn muốn đảm bảo port nằm trong một khoảng nhất định, bạn có thể thêm bước validate vào factory function:
export default () => {
const config = yaml.load(
readFileSync(join(import.meta.dirname, YAML_CONFIG_FILENAME), 'utf8'),
) as Record<string, any>;
if (config.http.port < 1024 || config.http.port > 49151) {
throw new Error('HTTP port must be between 1024 and 49151');
}
return config;
};
Lúc này, nếu port nằm ngoài khoảng đã chỉ định, ứng dụng sẽ ném lỗi ngay khi khởi động.
Sử dụng ConfigService
Để truy cập các giá trị cấu hình từ ConfigService, trước tiên ta cần inject ConfigService. Như với mọi provider, ta cần import module chứa nó - ConfigModule - vào module sẽ sử dụng nó (trừ khi bạn set property isGlobal trong object options truyền vào method ConfigModule.forRoot() thành true). Import nó vào một feature module như dưới đây.
@Module({
imports: [ConfigModule],
// ...
})
Sau đó ta có thể inject nó bằng constructor injection tiêu chuẩn:
constructor(private configService: ConfigService) {}
Gợi ý ConfigService được import từ package @nestjs/config.
Và sử dụng nó trong class của ta:
// lấy một biến môi trường
const dbUser = this.configService.get<string>('DATABASE_USER');
// lấy một giá trị cấu hình tùy chỉnh
const dbHost = this.configService.get<string>('database.host');
Như minh họa ở trên, dùng method configService.get() để lấy một biến môi trường đơn giản bằng cách truyền tên biến. Bạn có thể type hinting TypeScript bằng cách truyền type, như ở trên (ví dụ get<string>(...)). Method get() cũng có thể traverse một object cấu hình tùy chỉnh lồng nhau (được tạo qua file cấu hình tùy chỉnh), như ở ví dụ thứ hai bên trên.
Bạn cũng có thể lấy toàn bộ object cấu hình tùy chỉnh lồng nhau bằng cách dùng một interface làm type hint:
interface DatabaseConfig {
host: string;
port: number;
}
const dbConfig = this.configService.get<DatabaseConfig>('database');
// giờ bạn có thể dùng `dbConfig.port` và `dbConfig.host`
const port = dbConfig.port;
Method get() cũng nhận một tham số thứ hai tùy chọn định nghĩa giá trị mặc định, sẽ được trả về khi key không tồn tại, như dưới đây:
// dùng "localhost" khi "database.host" chưa được định nghĩa
const dbHost = this.configService.get<string>('database.host', 'localhost');
ConfigService có hai generic (type argument) tùy chọn. Cái đầu tiên giúp ngăn việc truy cập một config property không tồn tại. Dùng nó như dưới đây:
interface EnvironmentVariables {
PORT: number;
TIMEOUT: string;
}
// đâu đó trong code
constructor(private configService: ConfigService<EnvironmentVariables>) {
const port = this.configService.get('PORT', { infer: true });
// Lỗi TypeScript: không hợp lệ vì property URL không được định nghĩa trong EnvironmentVariables
const url = this.configService.get('URL', { infer: true });
}
Với property infer set là true, method ConfigService#get sẽ tự động suy ra kiểu của property dựa trên interface, ví dụ typeof port === "number" (nếu bạn không dùng flag strictNullChecks của TypeScript) vì PORT có kiểu number trong interface EnvironmentVariables.
Ngoài ra, với tính năng infer, bạn có thể suy ra kiểu của property trong một object cấu hình tùy chỉnh lồng nhau, kể cả khi dùng dot notation, như sau:
constructor(private configService: ConfigService<{ database: { host: string } }>) {
const dbHost = this.configService.get('database.host', { infer: true })!;
// typeof dbHost === "string" |
// +--> toán tử non-null assertion
}
Generic thứ hai dựa vào cái đầu tiên, đóng vai trò như một type assertion để loại bỏ mọi kiểu undefined mà các method của ConfigService có thể trả về khi strictNullChecks bật. Ví dụ:
// ...
constructor(private configService: ConfigService<{ PORT: number }, true>) {
// ^^^^
const port = this.configService.get('PORT', { infer: true });
// ^^^ Kiểu của port sẽ là 'number' nên bạn không cần TS type assertion nữa
}
Gợi ý Để đảm bảo method ConfigService#get chỉ lấy giá trị từ file cấu hình tùy chỉnh và bỏ qua biến process.env, set option skipProcessEnv thành true trong object options của ConfigModule.
Namespace cấu hình (Configuration namespaces)
ConfigModule cho phép bạn định nghĩa và load nhiều file cấu hình tùy chỉnh, như đã trình bày ở File cấu hình tùy chỉnh bên trên. Bạn có thể quản lý các cấu trúc object cấu hình phức tạp với các object cấu hình lồng nhau như trong phần đó. Hoặc, bạn có thể trả về một object cấu hình “có namespace” bằng function registerAs() như sau:
export default registerAs('database', () => ({
host: process.env.DATABASE_HOST,
port: process.env.DATABASE_PORT || 5432
}));
Cũng như với file cấu hình tùy chỉnh, bên trong factory function của registerAs(), object process.env sẽ chứa các cặp key/value biến môi trường đã resolve đầy đủ (với file .env và các biến định nghĩa từ bên ngoài đã được resolve và merge như mô tả ở trên).
Gợi ý Function registerAs được export từ package @nestjs/config.
Load một cấu hình có namespace bằng property load của object options của method forRoot(), theo cách tương tự như load file cấu hình tùy chỉnh:
import databaseConfig from './config/database.config.js';
@Module({
imports: [
ConfigModule.forRoot({
load: [databaseConfig],
}),
],
})
export class AppModule {}
Giờ, để lấy giá trị host từ namespace database, dùng dot notation. Dùng 'database' làm tiền tố cho tên property, tương ứng với tên của namespace (được truyền làm tham số đầu tiên cho function registerAs()):
const dbHost = this.configService.get<string>('database.host');
Một cách thay thế hợp lý là inject trực tiếp namespace database. Điều này cho phép ta tận dụng strong typing:
constructor(
@Inject(databaseConfig.KEY)
private dbConfig: ConfigType<typeof databaseConfig>,
) {}
Gợi ý ConfigType được export từ package @nestjs/config.
Ghi chú người dịch Cơ chế registerAs() + @Inject(config.KEY) giống với việc bind một struct config riêng vào constructor ở Go (thay vì đọc viper.GetString(...) rải rác khắp nơi), hoặc @Inject() một config class riêng trong NestJS thay vì gọi config()->get('database.host') trực tiếp như Laravel. Lợi ích chính: TypeScript biết chính xác shape của dbConfig, nên gõ nhầm tên property sẽ báo lỗi compile-time thay vì runtime undefined.
Cấu hình namespace trong module (Namespaced configurations in modules)
Để dùng một cấu hình có namespace làm object cấu hình cho một module khác trong ứng dụng, bạn có thể tận dụng method .asProvider() của object cấu hình. Method này chuyển cấu hình có namespace của bạn thành một provider, sau đó có thể truyền vào forRootAsync() (hoặc method tương đương) của module bạn muốn dùng.
Đây là một ví dụ:
import databaseConfig from './config/database.config.js';
@Module({
imports: [
TypeOrmModule.forRootAsync(databaseConfig.asProvider()),
],
})
Để hiểu cách method .asProvider() hoạt động, hãy xem giá trị trả về:
// Giá trị trả về của method .asProvider()
{
imports: [ConfigModule.forFeature(databaseConfig)],
useFactory: (configuration: ConfigType<typeof databaseConfig>) => configuration,
inject: [databaseConfig.KEY]
}
Cấu trúc này cho phép bạn tích hợp liền mạch các cấu hình có namespace vào module của mình, đảm bảo ứng dụng luôn có tổ chức và modular, mà không cần viết code khung (boilerplate) lặp đi lặp lại.
Cache biến môi trường (Cache environment variables)
Vì truy cập process.env có thể chậm, bạn có thể set property cache của object options truyền vào ConfigModule.forRoot() để tăng hiệu năng của method ConfigService#get khi làm việc với các biến lưu trong process.env.
ConfigModule.forRoot({
cache: true,
});
Đăng ký một phần (Partial registration)
Từ đầu tới giờ, ta đã xử lý các file cấu hình ở module gốc (ví dụ AppModule), bằng method forRoot(). Có thể bạn có một cấu trúc project phức tạp hơn, với các file cấu hình riêng cho từng tính năng nằm ở nhiều thư mục khác nhau. Thay vì load tất cả các file này ở module gốc, package @nestjs/config cung cấp một tính năng gọi là đăng ký một phần (partial registration), chỉ tham chiếu tới các file cấu hình liên quan đến từng feature module. Dùng static method forFeature() bên trong một feature module để thực hiện việc đăng ký một phần này, như sau:
import databaseConfig from './config/database.config.js';
@Module({
imports: [ConfigModule.forFeature(databaseConfig)],
})
export class DatabaseModule {}
Cảnh báo Trong một số trường hợp, bạn có thể cần truy cập các property được load qua đăng ký một phần bằng hook onModuleInit(), thay vì trong constructor. Lý do là method forFeature() chạy trong quá trình khởi tạo module, và thứ tự khởi tạo module là không xác định trước. Nếu bạn truy cập các giá trị được load theo cách này từ một module khác, trong constructor, module mà cấu hình đó phụ thuộc vào có thể chưa được khởi tạo xong. Method onModuleInit() chỉ chạy sau khi tất cả các module mà nó phụ thuộc vào đã được khởi tạo xong, nên kỹ thuật này an toàn hơn.
Ghi chú người dịch Đây là kiểu bug kinh điển khi vibe-code: bạn inject config qua constructor, chạy thử thấy hoạt động (vì tình cờ module kia init trước), rồi một hôm reorder imports lại lỗi undefined không rõ nguyên nhân. Tương tự vấn đề thứ tự khởi tạo singleton ở Go nếu bạn dùng init() phụ thuộc lẫn nhau giữa các package - không có gì đảm bảo thứ tự, trừ khi bạn tường minh hóa dependency.
Validate schema (Schema validation)
Thông lệ chuẩn là ném exception khi ứng dụng khởi động nếu các biến môi trường bắt buộc chưa được cung cấp hoặc không đáp ứng một số quy tắc validate nhất định. Package @nestjs/config hỗ trợ hai cách để làm việc này:
- Một schema tương thích Standard Schema được truyền qua option
validationSchema. Bất kỳ thư viện nào triển khai đặc tả này đều dùng được - Zod, Valibot, ArkType, và các thư viện khác. - Một function
validate()tùy chỉnh nhận biến môi trường làm input.
Cài thư viện validate bạn muốn dùng. Ở đây ta sẽ dùng Zod:
$ npm install --save zod
Giờ ta có thể định nghĩa một validation schema và truyền nó qua property validationSchema của object options của method forRoot(), như dưới đây:
import { z } from 'zod';
@Module({
imports: [
ConfigModule.forRoot({
validationSchema: z.object({
NODE_ENV: z
.enum(['development', 'production', 'test', 'provision'])
.default('development'),
PORT: z.coerce.number().default(3000),
}),
}),
],
})
export class AppModule {}
Ở đây, ta set giá trị mặc định cho NODE_ENV và PORT, sẽ được dùng nếu ta không cung cấp các biến này trong environment (file .env hoặc process environment). Để bắt buộc một biến thay vì cho phép mặc định, chỉ cần không set default cho nó - bước validate sẽ ném exception khi bootstrap nếu biến đó bị thiếu.
Lưu ý rằng biến môi trường luôn ở dạng string, đó là lý do PORT ở trên dùng z.coerce.number(). Giá trị mà schema trả về chính là giá trị mà ConfigService sẽ phục vụ, vì vậy các coercion và transformation khai báo trong schema sẽ được áp dụng lên cấu hình mà ứng dụng của bạn đọc được.
Mặc định, các biến môi trường không xác định (unknown) - nhiều entry không liên quan mà process.env luôn mang theo, như PATH và HOME - sẽ không kích hoạt exception validate, và mọi biến bị lỗi đều được báo cáo thay vì chỉ biến đầu tiên. Lỗi validate được format dạng PATH: message và nối bằng ký tự xuống dòng.
Bạn có thể forward các option riêng của thư viện thông qua key validationOptions. Vì option này được gõ kiểu theo đặc tả Standard Schema, các setting riêng của thư viện nằm dưới libraryOptions:
import { z } from 'zod';
@Module({
imports: [
ConfigModule.forRoot({
validationSchema: z.object({
NODE_ENV: z
.enum(['development', 'production', 'test', 'provision'])
.default('development'),
PORT: z.coerce.number().default(3000),
}),
validationOptions: {
libraryOptions: {
// các option riêng của thư viện validate bạn dùng
},
},
}),
],
})
export class AppModule {}
Gợi ý Đang dùng Joi? Nó vẫn được hỗ trợ, nhưng bạn phải dùng Joi v18 trở lên, phiên bản đã triển khai đặc tả Standard Schema. Truyền Joi.object({ ... }) vào validationSchema như trước, và đặt các setting của Joi như allowUnknown và abortEarly dưới validationOptions.libraryOptions. Riêng với schema Joi, @nestjs/config vẫn giữ default lịch sử là allowUnknown: true và abortEarly: false, và merge thêm những gì bạn truyền vào lên trên các default đó. Với project mới, ta khuyến nghị dùng một thư viện Standard Schema hiện đại như Zod thay thế.
Gợi ý Để tắt validate các biến môi trường được định nghĩa sẵn (predefined), set attribute validatePredefined thành false trong object options của method forRoot(). Biến môi trường predefined là các biến process (biến process.env) đã được set trước khi module được import. Ví dụ, nếu bạn khởi động ứng dụng bằng PORT=3000 node main.js, thì PORT là một biến môi trường predefined.
Function validate tùy chỉnh (Custom validate function)
Ngoài ra, bạn có thể chỉ định một function validate đồng bộ (synchronous) nhận vào một object chứa các biến môi trường (từ env file và process) và trả về một object chứa các biến môi trường đã validate, để bạn có thể convert/mutate chúng nếu cần. Nếu function ném lỗi, nó sẽ ngăn ứng dụng bootstrap.
Trong ví dụ này, ta sẽ dùng package class-transformer và class-validator. Đầu tiên, ta phải định nghĩa:
- một class với các ràng buộc validate,
- một function validate sử dụng function
plainToInstancevà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;
}
Với đoạn code này, dùng function validate như một option cấu hình của ConfigModule, như sau:
import { validate } from './env.validation.js';
@Module({
imports: [
ConfigModule.forRoot({
validate,
}),
],
})
export class AppModule {}
Getter function tùy chỉnh (Custom getter functions)
ConfigService định nghĩa một method get() chung để lấy một giá trị cấu hình theo key. Ta cũng có thể thêm các function getter để có phong cách viết code tự nhiên hơn một chút:
@Injectable()
export class ApiConfigService {
constructor(private configService: ConfigService) {}
get isAuthEnabled(): boolean {
return this.configService.get('AUTH_ENABLED') === 'true';
}
}
@Dependencies(ConfigService)
@Injectable()
export class ApiConfigService {
constructor(configService) {
this.configService = configService;
}
get isAuthEnabled() {
return this.configService.get('AUTH_ENABLED') === 'true';
}
}
Giờ ta có thể dùng getter function như sau:
@Injectable()
export class AppService {
constructor(apiConfigService: ApiConfigService) {
if (apiConfigService.isAuthEnabled) {
// Authentication đang được bật
}
}
}
@Dependencies(ApiConfigService)
@Injectable()
export class AppService {
constructor(apiConfigService) {
if (apiConfigService.isAuthEnabled) {
// Authentication đang được bật
}
}
}
Hook đã load biến môi trường (Environment variables loaded hook)
Nếu cấu hình của một module phụ thuộc vào biến môi trường, và các biến này được load từ file .env, bạn có thể dùng hook ConfigModule.envVariablesLoaded để đảm bảo file đã được load trước khi tương tác với object process.env, xem ví dụ sau:
export async function getStorageModule() {
await ConfigModule.envVariablesLoaded;
return process.env.STORAGE === 'S3' ? S3StorageModule : DefaultStorageModule;
}
Cấu trúc này đảm bảo rằng sau khi Promise ConfigModule.envVariablesLoaded resolve, tất cả biến cấu hình đều đã được load xong.
Cấu hình module có điều kiện (Conditional module configuration)
Có những lúc bạn muốn load một module có điều kiện và chỉ định điều kiện đó trong một biến env. May mắn là @nestjs/config cung cấp ConditionalModule cho phép bạn làm chính xác điều đó.
@Module({
imports: [
ConfigModule.forRoot(),
ConditionalModule.registerWhen(FooModule, 'USE_FOO'),
],
})
export class AppModule {}
Module trên chỉ load FooModule nếu trong file .env không có giá trị false cho biến env USE_FOO. Bạn cũng có thể tự truyền một điều kiện tùy chỉnh, một function nhận tham chiếu process.env và phải trả về một boolean để ConditionalModule xử lý:
@Module({
imports: [
ConfigModule.forRoot(),
ConditionalModule.registerWhen(
FooBarModule,
(env: NodeJS.ProcessEnv) => !!env['foo'] && !!env['bar'],
),
],
})
export class AppModule {}
Điều quan trọng là khi dùng ConditionalModule, bạn cũng phải có ConfigModule được load trong ứng dụng, để hook ConfigModule.envVariablesLoaded có thể được tham chiếu và sử dụng đúng cách. Nếu hook không chuyển thành true trong vòng 5 giây, hoặc một timeout tính bằng mili-giây do người dùng set ở tham số options thứ ba của method registerWhen, thì ConditionalModule sẽ ném lỗi và Nest sẽ hủy khởi động ứng dụng.
Biến có thể mở rộng (Expandable variables)
Package @nestjs/config hỗ trợ mở rộng biến môi trường (environment variable expansion). Với kỹ thuật này, bạn có thể tạo các biến môi trường lồng nhau, trong đó một biến được tham chiếu bên trong định nghĩa của biến khác. Ví dụ:
APP_URL=mywebsite.com
SUPPORT_EMAIL=support@${APP_URL}
Với cấu trúc này, biến SUPPORT_EMAIL sẽ resolve thành '[email protected]'. Lưu ý cú pháp ${...} được dùng để kích hoạt việc resolve giá trị của biến APP_URL bên trong định nghĩa của SUPPORT_EMAIL.
Gợi ý Để có tính năng này, package @nestjs/config sử dụng dotenv-expand bên trong.
Bật mở rộng biến môi trường bằng property expandVariables trong object options truyền vào method forRoot() của ConfigModule, như dưới đây:
@Module({
imports: [
ConfigModule.forRoot({
// ...
expandVariables: true,
}),
],
})
export class AppModule {}
Sử dụng trong main.ts
Dù cấu hình của ta được lưu trong một service, ta vẫn có thể dùng nó trong file main.ts. Nhờ vậy, bạn có thể dùng nó để lưu các biến như port ứng dụng hoặc CORS host.
Để truy cập nó, bạn phải dùng method app.get(), kèm theo tham chiếu tới service:
const configService = app.get(ConfigService);
Sau đó bạn có thể dùng nó như bình thường, bằng cách gọi method get với key cấu hình:
const port = configService.get('PORT');