Mengelola konfigurasi aplikasi Express dengan dotenv, env vars, config terpusat per environment, validasi konfigurasi dengan zod, dan praktik menyimpan secret tanpa pernah commit.

Sejauh ini beberapa kode kita memakai process.env.PORT, process.env.JWT_SECRET, dan process.env.DATABASE_URL — tapi nilainya belum diatur secara rapi. Episode 16 merapikan lapisan konfigurasi: dari mana nilai itu dibaca, bagaimana diatur per environment, dan bagaimana memastikan tidak ada secret yang bocor ke git.
Mengapa ini penting? Karena konfigurasi yang berantakan adalah akar dua masalah besar: misconfigurasi (port/URL salah di environment tertentu) dan kebocoran secret (key ter-commit, lalu database dibobol). Konfigurasi terpusat yang divalidasi sejak startup menangkap kedua masalah ini lebih awal — sebelum kode dijalankan.
npm install dotenvimport "dotenv/config"
import app from "./app.js"
const PORT = process.env.PORT || 3000
app.listen(PORT, () => {
console.log(`Server berjalan di http://localhost:${PORT}`)
})Import "dotenv/config" di baris pertama entry point memuat file .env ke process.env. Aturan praktiknya: load sedini mungkin agar seluruh modul lain bisa membaca env saat di-import.
| File | Peran | Status git |
|---|---|---|
.env | Nilai lokal development | ❌ tidak pernah commit |
.env.example | Template nama variabel (tanpa nilai rahasia) | ✅ commit |
.env.production | Override untuk produksi (jarang; biasanya via platform) | ❌ |
.env.example adalah kontrak: ia memberi tahu rekan tim variabel apa yang harus mereka isi tanpa pernah mengekspos nilai rahasia.
PORT=3000
NODE_ENV=development
MONGODB_URI=mongodb://localhost:27017/expresslab
DATABASE_URL=postgres://dev:dev@localhost:5432/expresslab
JWT_SECRET=ganti-dengan-random-string-panjang
SESSION_SECRET=ganti-dengan-random-string-panjang
CORS_ORIGIN=http://localhost:5173Warning
Satu secret yang ter-commit bisa memicu kebocoran akun dan audit keamanan. Periksa git status sebelum commit dan pastikan .env ada di .gitignore (kita tambahkan di episode 3). Jika pernah ter-commit ke git, rotasi secret itu — anggap sudah bocor selamanya.
Alih-alih membaca process.env.X tersebar di seluruh kode, kumpulkan dalam satu modul. Hasilnya: satu tempat untuk memeriksa, mendokumentasikan, dan memvalidasi seluruh konfigurasi.
import "dotenv/config"
export const config = {
env: process.env.NODE_ENV || "development",
port: Number(process.env.PORT) || 3000,
mongodbUri: process.env.MONGODB_URI,
databaseUrl: process.env.DATABASE_URL,
jwtSecret: process.env.JWT_SECRET,
corsOrigins: (process.env.CORS_ORIGIN || "").split(",").filter(Boolean),
isProduction: process.env.NODE_ENV === "production",
isDevelopment: process.env.NODE_ENV !== "production",
}Manfaatnya langsung terasa:
port sudah Number, corsOrigins sudah array, bukan string mentah.config.js sama dengan membaca seluruh pengaturan aplikasi.Konfigurasi yang salah sebaiknya menggagalkan startup, bukan berjalan dengan nilai kosong dan meledak di tengah request. Zod dari episode 13 kembali berjasa:
import "dotenv/config"
import { z } from "zod"
const envSchema = z.object({
NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
PORT: z.coerce.number().int().min(1).max(65535).default(3000),
MONGODB_URI: z.string().url(),
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32, "JWT_SECRET minimal 32 karakter"),
SESSION_SECRET: z.string().min(32, "SESSION_SECRET minimal 32 karakter"),
CORS_ORIGIN: z.string().default("http://localhost:5173"),
})
const parsed = envSchema.safeParse(process.env)
if (!parsed.success) {
console.error("Konfigurasi environment tidak valid:")
console.error(parsed.error.issues.map((i) => ` ${i.path} - ${i.message}`).join("\n"))
process.exit(1)
}
export const config = {
env: parsed.data.NODE_ENV,
port: parsed.data.PORT,
mongodbUri: parsed.data.MONGODB_URI,
databaseUrl: parsed.data.DATABASE_URL,
jwtSecret: parsed.data.JWT_SECRET,
corsOrigins: parsed.data.CORS_ORIGIN.split(",").filter(Boolean),
isProduction: parsed.data.NODE_ENV === "production",
}Dua perilaku yang dihasilkan:
JWT_SECRET dengan panjang di bawah 32 karakter ditolak saat startup — bukan saat user login.MONGODB_URI hilang) menghentikan server dengan pesan yang jelas, bukan request 500 misterius.Konfigurasi bisa berbeda per environment — kuncinya tetap pada satu config.js:
export const config = {
env: parsed.data.NODE_ENV,
isProduction: parsed.data.NODE_ENV === "production",
isDevelopment: parsed.data.NODE_ENV !== "production",
logging: {
level: parsed.data.NODE_ENV === "production" ? "info" : "debug",
},
security: {
cookieSecure: parsed.data.NODE_ENV === "production",
trustProxy: parsed.data.NODE_ENV === "production",
},
}Pola ini menghubungkan semua keputusan ke NODE_ENV yang konsisten: cookie secure hanya di produksi (episode 12), level log lebih verbose di development (episode 17), dan trust proxy aktif saat di belakang load balancer (episode 25).
Tip
Jangan pernah menempatkan logika "produksi atau tidak" di dalam route — taruh di config.js. Ketika kebijakan berubah (misal cookie kini perlu secure di staging juga), kalian cukup mengubah satu file, bukan mengejar string NODE_ENV di seluruh codebase.
Env dari docker run -e atau platform cloud tidak otomatis masuk ke .env — dan file .env tidak ter-bundle di image. Pastikan nilai dikirim eksplisit: -e di Docker, env vars di platform (episode 24).
"production" vs "prod" — satu typo mengubah perilaku cookie dan logging. Validasi dengan z.enum di atas mencegah ini sejak startup.
Jangan pernah menyalin .env ke Docker image. Platform menyuntikkan env saat runtime. Kita praktikkan langsung di episode 24.
Important
Rotasi secret adalah prosedur wajib jika secret pernah bocor — sekalipun hanya diduga. Jangan menunda: ganti JWT_SECRET/SESSION_SECRET, invalidasi token lama, dan terapkan prinsip assume breach.
Episode 16 merapikan lapisan konfigurasi: dotenv memuat .env, config.js menjadi satu sumber kebenaran, zod memvalidasi seluruh env di startup, dan .env.example menjadi kontrak tim tanpa membocorkan rahasia.
Inti yang harus dibawa pulang:
import "dotenv/config" di baris pertama entry point.process.env di satu config.js dengan tipe yang konsisten.safeParse + process.exit(1))..env.example di-commit; .env dan nilai rahasia tidak pernah.Di episode 17 selanjutnya kita akan membuat aplikasi bisa diselidiki: logging & debugging — morgan untuk request log, pino untuk structured logging, dan alur debugging dengan Node inspector. Sampai jumpa di episode 17!