Skip to content

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 dibutuhkan

Struktur 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.json

Jangan 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.prod

Contoh 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=supersecretdev123

Ringkasan 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-key

Untuk 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__joi
typescript
// 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

KonsepPenjelasan
ConfigModule.forRoot()Mendaftarkan dan menginisialisasi modul konfigurasi
envFilePathPath ke file .env yang akan dimuat
STAGE env varMenentukan environment mana yang aktif (dev/prod)
ConfigService.get()Membaca nilai konfigurasi dari mana saja
forRootAsyncInisialisasi modul yang menunggu modul lain selesai
useFactoryFungsi async yang mengembalikan konfigurasi modul
Joi schemaMendefinisikan tipe dan aturan wajib untuk setiap variabel
validationSchemaOpsi 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 lain

Struktur 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/prod

Jangan 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/config dan tambahkan ConfigModule.forRoot() di AppModule
  • [ ] Buat .env.stage.dev dan .env.stage.prod di root proyek
  • [ ] Tambahkan STAGE=dev ke script start:dev dan test di package.json
  • [ ] Tambahkan STAGE=prod ke script start:prod
  • [ ] Masukkan .env.stage.prod ke .gitignore
  • [ ] Buat config.schema.ts dengan Joi schema untuk semua variabel wajib
  • [ ] Tambahkan validationSchema ke ConfigModule.forRoot()
  • [ ] Ubah TypeOrmModule.forRoot() menjadi TypeOrmModule.forRootAsync() dengan useFactory
  • [ ] Verifikasi aplikasi startup berhasil; lalu coba hapus satu variabel wajib dan lihat error Joi

Pertanyaan Reflektif

  1. Mengapa kredensial database production sebaiknya tidak disimpan di file .env yang di-commit ke repository?
  2. Apa perbedaan antara forRoot() dan forRootAsync(), dan kapan kita harus menggunakan versi async?
  3. Jika STAGE tidak disediakan saat menjalankan aplikasi, apa yang akan terjadi berdasarkan konfigurasi yang sudah dibangun?
  4. Bagaimana cara kerja validasi Joi membantu proses debugging dibandingkan tanpa validasi sama sekali?