Aplikasi yang sama hampir selalu hidup di lebih dari satu tempat: komputer kita saat mengembangkan, server uji, lalu produksi. Port yang dipakai berbeda, alamat database berbeda, mode debug menyala di satu tempat dan padam di tempat lain. Bila nilai-nilai tersebut ditanam di dalam kode, setiap perpindahan lingkungan menuntut pengeditan — dan satu nilai yang terlupakan mengubah perilaku aplikasi secara diam-diam. Pada chapter ini kita susun arsitektur konfigurasi yang benar: seluruh pengaturan masuk lewat environment variable , divalidasi di satu modul, diberi nilai bawaan yang jelas, lalu dibekukan menjadi satu object untuk seluruh aplikasi.

Konfigurasi yang Tidak Sehat #

Bentuk paling mudah — dan paling rapuh — adalah konstanta yang ditanam langsung di kode:

js Ilustrasi konfigurasi yang ditanam di kode
const PORT = 3000;
const DATABASE_URL = 'file:tugas-lokal.db';
const DEBUG = true;
const BATAS_HALAMAN = 20;

// ... ratusan baris kemudian ...
app.listen(PORT);
db.buka(DATABASE_URL);
if (DEBUG) console.log('permintaan masuk:', url);

Daftar masalahnya panjang. Mengganti nilai berarti mengedit kode, dan kode yang berbeda antar lingkungan berarti kita tidak lagi menguji kode yang sama dengan yang dijalankan di produksi. Nilai seperti alamat database yang berisi kredensial ikut tersimpan di version control — pelanggaran yang dibahas tuntas di chapter Manajemen Secret. Dan ketika BATAS_HALAMAN dipakai di lima berkas, menggantinya menjadi pencarian manual yang rawan tertinggal.

Prinsip yang kita kejar berasal dari metodologi twelve-factor: satu kode, banyak lingkungan, perbedaan hidup di environment. Node.js membaca environment melalui process.env — materi dasarnya sudah kita bahas di chapter Environment Variables pada Section Command-Line Applications; chapter ini fokus pada cara menyusunnya menjadi arsitektur.

Satu Modul untuk Seluruh Konfigurasi #

Rencananya sederhana. Seluruh pembacaan process.env dikumpulkan ke satu modul konfigurasi.mjs. Modul tersebut menjalankan tiga tugas berurutan: memvalidasi kunci wajib, menerapkan nilai bawaan untuk kunci opsional, lalu membekukan hasilnya. Kontrak kunci-kuncinya sebagai berikut:

KunciWajibNilai bawaanKonversi
APP_PORTYa—Number, rentang 1-65535
APP_DATABASE_URLYa—string apa adanya
APP_DEBUGTidakfalse'1' berarti true
APP_PAGE_SIZETidak20Number

Demo: Validasi, Nilai Bawaan, dan Object Beku #

Modul berikut mengekspor satu function bacaKonfigurasi yang menerima object environment sebagai parameter — bukan membaca process.env langsung. Keputusan ini membuat modul dapat diuji dengan kombinasi nilai apa pun tanpa menyentuh environment sungguhan:

js
// Modul konfigurasi: satu pintu pembaca environment variable.
// Fungsi ini menerima object environment (bukan langsung process.env)
// agar mudah diuji dengan berbagai kombinasi nilai.
export function bacaKonfigurasi(env) {
  const galat = [];

  const port = Number(env.APP_PORT);
  if (!Number.isInteger(port) || port < 1 || port > 65535) {
    galat.push('APP_PORT harus berupa bilangan bulat 1-65535');
  }
  if (env.APP_DATABASE_URL === undefined || env.APP_DATABASE_URL === '') {
    galat.push('APP_DATABASE_URL wajib diisi');
  }

  if (galat.length > 0) {
    const e = new Error(`konfigurasi tidak valid: ${galat.join('; ')}`);
    e.name = 'KonfigurasiError';
    throw e;
  }

  return Object.freeze({
    port,
    urlDatabase: env.APP_DATABASE_URL,
    modeDebug: env.APP_DEBUG === '1',
    batasHalaman: Number(env.APP_PAGE_SIZE ?? 20),
  });
}

Sekarang kita uji kontraknya dengan kombinasi yang mewakili: kunci lengkap, kunci opsional kosong, percobaan mutasi, dan tiga keadaan tidak valid:

js
import { bacaKonfigurasi } from './konfigurasi.mjs';

const lengkap = bacaKonfigurasi({
  APP_PORT: '3000',
  APP_DATABASE_URL: 'file:tugas.db',
  APP_DEBUG: '1',
  APP_PAGE_SIZE: '50',
});
console.log('semua kunci terisi :', JSON.stringify(lengkap));

const tanpaOpsional = bacaKonfigurasi({
  APP_PORT: '3000',
  APP_DATABASE_URL: 'file:tugas.db',
});
console.log('nilai bawaan      :', JSON.stringify(tanpaOpsional));

try {
  lengkap.port = 8080;
} catch (e) {
  console.log('mutasi ditolak     :', e.name);
}

const kasusGagal = [
  ['port kosong', { APP_DATABASE_URL: 'file:tugas.db' }],
  ['port bukan angka', { APP_PORT: 'delapan ribu', APP_DATABASE_URL: 'file:tugas.db' }],
  ['url database hilang', { APP_PORT: '3000' }],
];
for (const [label, env] of kasusGagal) {
  try {
    bacaKonfigurasi(env);
  } catch (e) {
    console.log(`${label.padEnd(18)} -> ${e.name}: ${e.message}`);
  }
}

Output yang Dihasilkan:

text
semua kunci terisi : {"port":3000,"urlDatabase":"file:tugas.db","modeDebug":true,"batasHalaman":50}
nilai bawaan      : {"port":3000,"urlDatabase":"file:tugas.db","modeDebug":false,"batasHalaman":20}
mutasi ditolak     : TypeError
port kosong        -> KonfigurasiError: konfigurasi tidak valid: APP_PORT harus berupa bilangan bulat 1-65535
port bukan angka   -> KonfigurasiError: konfigurasi tidak valid: APP_PORT harus berupa bilangan bulat 1-65535
url database hilang -> KonfigurasiError: konfigurasi tidak valid: APP_DATABASE_URL wajib diisi

Mari kita bedah perilaku yang terbukti di output:

  1. Baris semua kunci terisi: String '3000' dan '50' dikonversi menjadi angka; '1' pada APP_DEBUG menjadi true. Bentuk akhirnya object tunggal — konsumen tinggal membaca konfig.port, bukan memilah string dari environment.
  2. Baris nilai bawaan: Tanpa APP_DEBUG dan APP_PAGE_SIZE, konfigurasi tetap sah — modeDebug berupa false dan batasHalaman berupa 20. Operator ?? di baris terakhir konfigurasi.mjs hanya mengganti nilai undefined; nilai lain yang sengaja diisi tetap diteruskan apa adanya.
  3. Baris mutasi ditolak: Object.freeze membuat object konfigurasi menjadi baca-saja; percobaan mengubah port melempar TypeError. Konfigurasi yang tidak bisa berubah setelah dibangun berarti tidak ada modul yang bisa diam-diam mengubah pengaturan di tengah jalan.
  4. Tiga baris terakhir: Seluruh kegagalan terkumpul sekaligus — KonfigurasiError membawa daftar lengkap, bukan hanya keluhan pertama. Port kosong dan port berupa teks sama-sama ditolak oleh pemeriksaan Number.isInteger, dan pesan kesalahan menyebut kunci yang dimaksud sehingga pengoperasi langsung tahu apa yang harus diperbaiki.

Menyala dari Environment Asli #

Titik masuk aplikasi memanggil bacaKonfigurasi(process.env) dalam satu tempat. Bila konfigurasi tidak valid, aplikasi berhenti lebih dulu — gagal cepat, sebelum server sempat mendengarkan port atau koneksi database dibuka:

js
import { bacaKonfigurasi } from './konfigurasi.mjs';

try {
  const konfig = bacaKonfigurasi(process.env);
  console.log(`aplikasi tugas siap di port ${konfig.port}`);
  console.log(`database     : ${konfig.urlDatabase}`);
  console.log(`mode debug   : ${konfig.modeDebug ? 'aktif' : 'nonaktif'}`);
  console.log(`batas halaman: ${konfig.batasHalaman}`);
} catch (e) {
  console.error(`${e.name}: ${e.message}`);
  process.exitCode = 1;
}

Jalankan dengan environment lengkap di depan perintah:

bash
APP_PORT=3000 APP_DATABASE_URL=file:tugas.db APP_DEBUG=1 node mulai-aplikasi.mjs

Output yang Dihasilkan:

text
aplikasi tugas siap di port 3000
database     : file:tugas.db
mode debug   : aktif
batas halaman: 20

Lalu tanpa environment apa pun — keadaan yang meniru operator lupa menyiapkan lingkungan:

bash
node mulai-aplikasi.mjs

Output yang Dihasilkan:

text
KonfigurasiError: konfigurasi tidak valid: APP_PORT harus berupa bilangan bulat 1-65535; APP_DATABASE_URL wajib diisi
kode keluar: 1

Dua output tersebut menceritakan dua ujung arsitektur. Dengan environment lengkap, aplikasi menyala dengan seluruh nilai terkonversi dan tercetak rapi. Tanpa environment, bacaKonfigurasi mengumpulkan dua kelalaian sekaligus (port dan database), pesan kesalahan menjelaskan keduanya, dan proses berakhir dengan kode keluar 1 — sinyal standar bagi sistem operasi dan platform deployment bahwa proses gagal.

Konfigurasi per Lingkungan #

Dengan arsitektur ini, membedakan lingkungan menjadi urusan environment, bukan kode. Lingkungan pengembangan menyalakan APP_DEBUG=1 dan memakai database lokal; lingkungan produksi mematikan debug dan menyuntikkan nilai lewat platform deployment. Untuk kenyamanan harian, flag node --env-file memuat pasangan kunci-nilai dari sebuah berkas — mekanisme flag-nya dibahas khusus di chapter Berkas Environment dengan –env-file pada Section Modern Node.js, termasuk aturan bahwa nilai yang sudah terpasang di environment menang atas nilai di berkas. Perkakas serupa dibahas di panduan environment variable Node.js , dan urusan penyimpanan kredensial dengan aman menjadi ranah chapter Manajemen Secret.

Object konfigurasi beku kini siap disuntikkan ke seluruh lapisan aplikasi. Pertanyaan berikutnya bersifat fisik: di folder mana semua modul ini sebaiknya berdiri — konfigurasi.mjs, service, repository, rute, beserta tes-nya? Chapter Struktur Proyek yang Skalabel merancang tata letaknya.