Appearance
Chapter 7 - Configuration Management
Ikhtisar Chapter
Chapter ini membangun sistem konfigurasi yang memisahkan nilai spesifik-environment dari kode aplikasi. Dengan @nestjs/config dan file .env per stage, tidak ada lagi nilai hardcoded seperti kredensial database atau secret JWT yang tersebar di codebase.
Peta Cepat
FokusConfigModule, env files, stage-based config, Joi validation
Masalah yang DiselesaikanNilai seperti DB password berbeda di dev vs production, dan tidak boleh hardcoded
Hasil AkhirKonfigurasi tervalidasi dan terpusat, aman digunakan di semua environment
Gambaran Besar
Bayangkan Anda harus mengubah kredensial database hanya untuk deployment ke production. Jika nilai tersebut tersebar di berbagai file kode, ada risiko melewatkan satu tempat — atau lebih buruk, secara tidak sengaja meng-commit password ke repository. Itulah masalah yang dipecahkan di chapter ini.
Solusinya adalah memisahkan konfigurasi dari kode menggunakan dua konsep utama: file environment (.env) per stage dan ConfigModule dari NestJS yang mengelola pembacaan dan distribusi nilai-nilai tersebut. Di atasnya, ditambahkan validasi schema menggunakan Joi agar aplikasi gagal dengan pesan jelas jika ada variabel wajib yang tidak disediakan.
Dua Sumber Konfigurasi
text
┌─────────────────────────────────────────────────────────────┐
│ Sumber Nilai Konfigurasi │
├──────────────────────────┬──────────────────────────────────┤
│ File .env (codebase) │ Environment Variable (runtime) │
├──────────────────────────┼──────────────────────────────────┤
│ Cocok untuk nilai │ Cocok untuk nilai sensitif │
│ non-sensitif seperti │ seperti DB password production │
│ port, nama database dev │ atau JWT secret production │
│ │ │
│ Tersimpan di repo │ Tidak pernah menyentuh repo │
│ (dev/staging safe) │ (ditambahkan saat deploy) │
└──────────────────────────┴──────────────────────────────────┘Nilai yang diberikan melalui environment variable saat runtime akan menimpa nilai dari file
.env. Ini berguna untuk override nilai sensitif di production tanpa mengubah codebase.
Alur Konfigurasi
text
Aplikasi start
|
v
Baca env var STAGE (mis: "dev" atau "prod")
|
v
ConfigModule memuat .env.stage.dev atau .env.stage.prod
|
v
Joi memvalidasi apakah semua variabel wajib tersedia
|-- Tidak valid --> aplikasi crash + pesan error yang jelas
|-- Valid --> konfigurasi tersedia via ConfigService
|
v
Module lain (TypeORM, AuthModule, dll) menggunakan ConfigService
untuk membaca nilai yang dibutuhkanStruktur Direktori
Chapter ini menambahkan file konfigurasi di luar src/ dan satu file schema validasi:
nestjs-task-management/
├── src/
│ ├── main.ts
│ ├── app.module.ts ← DIUBAH: ConfigModule.forRoot + forRootAsync
│ ├── config.schema.ts ← BARU: Joi validation schema
│ ├── auth/
│ │ ├── auth.module.ts ← DIUBAH: JwtModule pakai ConfigService
│ │ └── ...
│ └── tasks/
│ └── ...
├── .env.stage.dev ← BARU: nilai untuk environment development
├── .env.stage.prod ← BARU: nilai untuk environment production
├── .gitignore ← .env.stage.prod harus ada di sini!
└── package.jsonJangan Commit .env Production!
File .env.stage.dev aman di-commit karena tidak mengandung nilai sensitif production. File .env.stage.prod sebaiknya tidak di-commit — tambahkan ke .gitignore.
# .gitignore
.env.stage.prodContoh Isi File .env
bash
# .env.stage.dev
STAGE=dev
DB_HOST=localhost
DB_PORT=5432
DB_USERNAME=postgres
DB_PASSWORD=postgres
DB_DATABASE=task-management
JWT_SECRET=supersecretdev123Ringkasan Lecture
1. Introduction to Configuration
Konfigurasi adalah nilai yang dimuat saat aplikasi start dan tidak berubah selama runtime. Contoh: port server, URL database, secret JWT. Nilai ini bisa berasal dari berbagai sumber — file JSON/YAML, variabel environment, atau codebase langsung.
Perbedaan utama:
- Codebase config (file
.env) — cocok untuk nilai non-sensitif, bisa di-commit dengan aman di lingkungan development. - Environment variable — cocok untuk nilai sensitif seperti password production; diberikan saat menjalankan aplikasi, tidak pernah masuk ke repository.
2. Quick Intro to Environment Variables
Environment variable adalah cara menyuntikkan data ke sebuah proses tanpa mengubah kode. Di Node.js, nilainya diakses melalui objek process.env.
bash
# Menyediakan variabel saat menjalankan aplikasi
MY_VAR=hello yarn start:dev
# Membaca di kode
console.log(process.env.MY_VAR) // "hello"Konvensi: nama environment variable selalu menggunakan HURUF BESAR (uppercase). Beberapa environment variable sudah disediakan Node.js secara default, seperti PATH, NODE, dan sebagainya.
3. Setting up ConfigModule
@nestjs/config diinstal lalu ConfigModule.forRoot() didaftarkan di AppModule. Konfigurasi kunci adalah envFilePath, yaitu path ke file .env yang akan dimuat berdasarkan variabel STAGE.
typescript
// app.module.ts
ConfigModule.forRoot({
envFilePath: `.env.stage.${process.env.STAGE}`,
})Agar nilai STAGE tersedia, variabel tersebut ditambahkan ke script di package.json:
json
"scripts": {
"start:dev": "STAGE=dev nest start --watch",
"start:prod": "STAGE=prod node dist/main",
"test": "STAGE=dev jest"
}File .env.stage.dev dan .env.stage.prod dibuat di root proyek — masing-masing berisi nilai konfigurasi untuk environment yang sesuai.
bash
# .env.stage.dev
STAGE=dev
DB_HOST=localhost
DB_PORT=5432
DB_USERNAME=postgres
DB_PASSWORD=postgres
DB_DATABASE=task-management
JWT_SECRET=secret-dev-keyUntuk menggunakan nilai konfigurasi di suatu module, cukup impor ConfigModule ke module tersebut, lalu inject ConfigService melalui dependency injection:
typescript
constructor(private configService: ConfigService) {}
// Membaca nilai
const dbHost = this.configService.get<string>('DB_HOST')4. TypeORM Configuration
Tantangan muncul saat TypeOrmModule.forRoot() perlu menggunakan nilai dari ConfigModule. Karena ConfigModule harus selesai diinisialisasi terlebih dahulu, konfigurasi TypeORM tidak bisa dilakukan secara sinkron.
Solusinya adalah TypeOrmModule.forRootAsync() — versi asinkron yang menunggu modul lain selesai sebelum membangun konfigurasinya:
typescript
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({
type: 'postgres',
host: configService.get('DB_HOST'),
port: configService.get<number>('DB_PORT'),
username: configService.get('DB_USERNAME'),
password: configService.get('DB_PASSWORD'),
database: configService.get('DB_DATABASE'),
autoLoadEntities: true,
synchronize: true,
}),
})useFactory adalah fungsi async yang dipanggil NestJS ketika module siap diinisialisasi. Nilai yang dikembalikan menjadi konfigurasi TypeORM. Karena merupakan fungsi biasa, kita bebas menggunakan dependency injection di dalamnya — termasuk ConfigService.
5. Config Schema Validation
Masalah yang dipecahkan: bagaimana kita tahu bahwa semua variabel wajib tersedia sebelum aplikasi mulai bekerja? Tanpa validasi, aplikasi bisa gagal di tengah-tengah operasi hanya karena satu variabel lupa didefinisikan.
Solusinya adalah Joi — library validasi schema. Kita mendefinisikan schema yang menggambarkan variabel apa saja yang wajib ada, tipenya, dan nilai defaultnya jika tidak disediakan.
bash
yarn add @hapi/joi
yarn add -D @types/@hapi__joitypescript
// config.schema.ts
import * as Joi from '@hapi/joi'
export const configValidationSchema = Joi.object({
STAGE: Joi.string().required(),
DB_HOST: Joi.string().required(),
DB_PORT: Joi.number().default(5432).required(),
DB_USERNAME: Joi.string().required(),
DB_PASSWORD: Joi.string().required(),
DB_DATABASE: Joi.string().required(),
JWT_SECRET: Joi.string().required(),
})Schema ini kemudian diberikan ke ConfigModule.forRoot():
typescript
ConfigModule.forRoot({
envFilePath: `.env.stage.${process.env.STAGE}`,
validationSchema: configValidationSchema,
})Hasilnya: jika ada variabel wajib yang tidak tersedia saat aplikasi start, NestJS akan langsung crash dengan pesan error yang spesifik — jauh lebih baik daripada menemukan masalah di tengah request.
Konsep Kunci
| Konsep | Penjelasan |
|---|---|
ConfigModule.forRoot() | Mendaftarkan dan menginisialisasi modul konfigurasi |
envFilePath | Path ke file .env yang akan dimuat |
STAGE env var | Menentukan environment mana yang aktif (dev/prod) |
ConfigService.get() | Membaca nilai konfigurasi dari mana saja |
forRootAsync | Inisialisasi modul yang menunggu modul lain selesai |
useFactory | Fungsi async yang mengembalikan konfigurasi modul |
| Joi schema | Mendefinisikan tipe dan aturan wajib untuk setiap variabel |
validationSchema | Opsi di forRoot() untuk mengaktifkan validasi Joi |
Perbandingan: Sinkron vs Asinkron
text
TypeOrmModule.forRoot({ ... })
↓
Semua nilai tersedia langsung (hardcoded)
Sederhana, tapi tidak bisa menggunakan ConfigService
─────────────────────────────────────────────────
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (config) => ({ ... })
})
↓
Menunggu ConfigModule selesai
Bisa menggunakan ConfigService untuk membaca env vars
Wajib digunakan jika konfigurasi bergantung pada modul lainStruktur File Konfigurasi
text
src/
config.schema.ts ← validasi schema Joi
app.module.ts ← ConfigModule.forRoot + validasi schema
.env.stage.dev ← nilai untuk development
.env.stage.prod ← nilai untuk production (tidak di-commit!)
package.json ← scripts menyertakan STAGE=dev/prodJangan commit .env.stage.prod ke repository
File .env.stage.prod berisi kredensial database dan JWT secret production. Pastikan file ini masuk ke .gitignore. Nilai production sebaiknya diberikan melalui variabel environment di platform deployment (Netlify, Heroku, AWS, dsb).
Jebakan Umum
Restart wajib setelah ubah file .env
Nilai dari file .env dibaca saat aplikasi start. Mengubah file tersebut tidak otomatis memperbarui nilai yang sudah dimuat. Selalu restart aplikasi setelah mengubah file konfigurasi.
forRoot vs forRootAsync
Menggunakan forRoot (sinkron) saat Anda butuh ConfigService akan gagal karena modul belum siap. Gunakan forRootAsync setiap kali konfigurasi suatu modul bergantung pada ConfigService.
Default value di Joi
Joi.number().default(5432) berarti jika DB_PORT tidak disediakan, nilai defaultnya adalah 5432. Ini berguna untuk variabel yang punya nilai sensible default sehingga tidak harus selalu ditulis ulang di setiap file .env.
Checklist Implementasi
- [ ] Install
@nestjs/configdan tambahkanConfigModule.forRoot()diAppModule - [ ] Buat
.env.stage.devdan.env.stage.proddi root proyek - [ ] Tambahkan
STAGE=devke scriptstart:devdantestdipackage.json - [ ] Tambahkan
STAGE=prodke scriptstart:prod - [ ] Masukkan
.env.stage.prodke.gitignore - [ ] Buat
config.schema.tsdengan Joi schema untuk semua variabel wajib - [ ] Tambahkan
validationSchemakeConfigModule.forRoot() - [ ] Ubah
TypeOrmModule.forRoot()menjadiTypeOrmModule.forRootAsync()denganuseFactory - [ ] Verifikasi aplikasi startup berhasil; lalu coba hapus satu variabel wajib dan lihat error Joi
Pertanyaan Reflektif
- Mengapa kredensial database production sebaiknya tidak disimpan di file
.envyang di-commit ke repository? - Apa perbedaan antara
forRoot()danforRootAsync(), dan kapan kita harus menggunakan versi async? - Jika
STAGEtidak disediakan saat menjalankan aplikasi, apa yang akan terjadi berdasarkan konfigurasi yang sudah dibangun? - Bagaimana cara kerja validasi Joi membantu proses debugging dibandingkan tanpa validasi sama sekali?