Kenapa ini penting
Aplikasimu sekarang punya rahasia: SESSION_SECRET untuk menandatangani token, ADMIN_PASSWORD, token database Turso, mungkin juga API key layanan email. Rahasia ini tidak boleh ditulis langsung di kode. Kode masuk ke Git, Git dibagikan ke tim atau diunggah ke GitHub, dan sekali rahasia bocor ke repositori publik, bot penyisir GitHub bisa menemukannya dalam hitungan menit.
Selain rahasia, ada juga nilai yang berbeda antar lingkungan: di laptop database-nya file:local.db, di produksi libsql://blog-kamu.turso.io; di laptop alamat situs http://localhost:3000, di produksi https://blogkamu.com. Environment variable (variabel lingkungan) adalah cara standar memisahkan nilai-nilai ini dari kode, sehingga kode yang sama bisa jalan di mana saja hanya dengan mengganti pengaturannya.
Analogi: kode aplikasimu adalah resep masakan yang boleh difotokopi dan dibagikan. Environment variable adalah kunci lemari bumbu rahasia yang disimpan terpisah. Setiap dapur (laptop, server staging, server produksi) punya kuncinya sendiri, dan kunci itu tidak pernah ditempel di halaman resep.
Konsep inti
File .env* dan urutan pemuatan
Next.js otomatis memuat variabel dari file .env* di root proyek (bukan di dalam src/) ke process.env. Urutan pencariannya (berhenti di yang pertama ketemu):
process.env(variabel yang sudah diset di sistem atau oleh hosting).env.$(NODE_ENV).local(misalnya.env.development.local).env.local(tidak dibaca saatNODE_ENV=test).env.$(NODE_ENV)(misalnya.env.production).env
NODE_ENV otomatis development saat next dev, dan production untuk perintah lain seperti next build dan next start.
| File | Isi | Di-commit? |
|---|---|---|
.env.local |
Rahasia di laptopmu | Tidak pernah |
.env.development / .env.production |
Default non-rahasia per lingkungan | Boleh |
.env.test |
Default untuk test | Boleh |
.env.example |
Daftar nama variabel tanpa nilai asli, sebagai dokumentasi tim | Ya |
Template create-next-app sudah menulis baris .env* di .gitignore, sehingga semua file .env diabaikan Git secara default. Kalau kamu ingin meng-commit .env.example, tambahkan pengecualian !.env.example di .gitignore.
Next.js juga mendukung referensi antar variabel dengan $, misalnya API_URL=$BASE_URL/api. Efek sampingnya: nilai yang mengandung karakter $ (sering ada di hash password bcrypt) akan dianggap referensi. Escape dengan \$ bila nilainya memang memuat tanda dolar.
Server-only secara default, NEXT_PUBLIC_ untuk browser
Ini aturan paling penting: secara default, environment variable hanya tersedia di server. Server Component, Server Action, Route Handler, dan Proxy bisa membaca process.env.SESSION_SECRET. Client Component tidak bisa; nilainya undefined.
Kalau sebuah nilai memang perlu di browser (misalnya ID Google Analytics atau URL publik situs), beri awalan NEXT_PUBLIC_. Saat next build, Next.js mengganti teks process.env.NEXT_PUBLIC_SITE_URL di kode client dengan nilai aslinya. Dua konsekuensi besar:
- Nilainya terlihat publik di file JavaScript yang diunduh browser. Jangan pernah memberi awalan
NEXT_PUBLIC_pada rahasia. - Nilainya dibekukan saat build. Mengubah variabel di hosting setelah build tidak berpengaruh sampai kamu build ulang. Penggantian juga hanya bekerja untuk penulisan langsung
process.env.NEXT_PUBLIC_X; akses dinamis sepertiprocess.env[nama]atauconst env = process.env; env.NEXT_PUBLIC_Xtidak ikut diganti.
Variabel di luar Next.js
drizzle-kit, script seed, atau test runner tidak dijalankan oleh Next.js, jadi tidak otomatis membaca .env.local. Pilihannya:
- Paket
@next/envdenganloadEnvConfig(process.cwd()), cara yang sama dengan Next.js. - Flag bawaan Node.js:
node --env-file-if-exists=.env.local script.mjs(tersedia sejak Node.js 22.9; di Node 20 pakai@next/env).
next.config.mjs
File konfigurasi Next.js. Proyek baru membuatnya dalam format ES module (.mjs). Beberapa opsi yang relevan di fase ini:
| Opsi | Kegunaan |
|---|---|
cacheComponents: true |
Mengaktifkan model Cache Components (default di proyek baru) |
partialPrefetching: true |
Prefetch App Shell; sebaiknya diset eksplisit bersama cacheComponents |
output: 'standalone' atau 'export' |
Bentuk hasil build untuk deploy (Modul 10) |
images.remotePatterns |
Domain gambar luar yang diizinkan next/image |
redirects() / headers() |
Redirect dan header statis |
serverActions.bodySizeLimit |
Menaikkan batas 1 MB body Server Action |
serverExternalPackages |
Paket yang tidak ikut di-bundle di server |
next.config dibaca saat build dan saat server start, bukan di browser. Kamu boleh memakai process.env di dalamnya.
Validasi env saat start
Lupa mengisi SESSION_SECRET di hosting adalah penyebab klasik "login tidak jalan di produksi". Pola yang bagus: validasi semua variabel wajib dengan Zod di satu file khusus server, sehingga aplikasi gagal cepat dengan pesan jelas.
Contoh kode
# .env.local (di-ignore Git, hanya di laptopmu)
SESSION_SECRET=hasil-openssl-rand-base64-32
ADMIN_PASSWORD=rahasia-belajar-123
REVALIDATE_SECRET=ganti-dengan-rahasia-panjang
DATABASE_URL=file:local.db
NEXT_PUBLIC_SITE_URL=http://localhost:3000
# .env.example (di-commit, nilai palsu sebagai panduan tim)
SESSION_SECRET=
ADMIN_PASSWORD=
REVALIDATE_SECRET=
DATABASE_URL=file:local.db
DATABASE_AUTH_TOKEN=
NEXT_PUBLIC_SITE_URL=http://localhost:3000
Validasi env di satu tempat:
// lib/env.js
import 'server-only'
import { z } from 'zod'
const EnvSchema = z.object({
SESSION_SECRET: z.string().min(32, 'SESSION_SECRET minimal 32 karakter'),
ADMIN_PASSWORD: z.string().min(8, 'ADMIN_PASSWORD minimal 8 karakter'),
DATABASE_URL: z.string().default('file:local.db'),
DATABASE_AUTH_TOKEN: z.string().optional(),
})
const parsed = EnvSchema.safeParse(process.env)
if (!parsed.success) {
console.error('Environment variable tidak valid:', z.flattenError(parsed.error).fieldErrors)
throw new Error('Konfigurasi environment salah, cek .env.local atau pengaturan hosting')
}
export const env = parsed.data
// Pemakaian: import { env } from '@/lib/env' → env.SESSION_SECRET
Perbedaan server vs client dalam praktik:
// app/debug/page.js (Server Component)
import { ClientInfo } from './ClientInfo'
export default function DebugPage() {
return (
<div>
<p>Server tahu secret? {process.env.SESSION_SECRET ? 'ya' : 'tidak'}</p>
{/* → Server tahu secret? ya */}
<ClientInfo />
</div>
)
}
// app/debug/ClientInfo.jsx
'use client'
export function ClientInfo() {
return (
<ul>
<li>SESSION_SECRET di browser: {String(process.env.SESSION_SECRET)}</li>
{/* → undefined (aman, tidak ikut ter-bundle) */}
<li>NEXT_PUBLIC_SITE_URL di browser: {process.env.NEXT_PUBLIC_SITE_URL}</li>
{/* → http://localhost:3000 (ditanam saat build, terlihat publik) */}
</ul>
)
}
Script di luar Next.js yang tetap membaca .env.local, contoh dari package.json proyek akhir:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"db:push": "drizzle-kit push",
"db:seed": "node --env-file-if-exists=.env.local scripts/seed.mjs"
}
}
Contoh next.config.mjs yang sudah diperkaya:
// next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
cacheComponents: true,
partialPrefetching: true,
images: {
remotePatterns: [{ protocol: 'https', hostname: 'images.unsplash.com' }],
},
async redirects() {
return [{ source: '/artikel/:slug', destination: '/blog/:slug', permanent: true }]
},
async headers() {
return [
{
source: '/(.*)',
headers: [
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
],
},
]
},
}
export default nextConfig
Kesalahan umum
- Memberi awalan
NEXT_PUBLIC_pada rahasia supaya "bisa dipakai di client". Nilainya jadi terbaca semua orang di file JavaScript. Kalau client butuh hasil yang memerlukan rahasia, panggil Server Action atau Route Handler yang memakainya di server. - Meng-commit
.env.local. Pastikan.env*ada di.gitignore. Kalau terlanjur ter-push, menghapus file tidak cukup: ganti (rotate) semua rahasianya, karena riwayat Git tetap menyimpannya. - Mengubah
NEXT_PUBLIC_di hosting tanpa build ulang. Nilainya ditanam saat build. Redeploy setelah mengubah variabel publik. - Membaca env dengan nama dinamis di client.
process.env[key]tidak diganti saat build dan hasilnyaundefined. Tulis nama lengkapnya secara langsung. - Script seed atau drizzle-kit tidak menemukan variabel. Tool di luar Next.js tidak membaca
.env.localotomatis. Pakai--env-file-if-existsatauloadEnvConfigdari@next/env.
Latihan
- Mudah, Tambahkan
NEXT_PUBLIC_SITE_NAME=Blog Kodingupdi.env.local, lalu tampilkan di footer lewat Client Component. Ubah nilainya, restartnpm run dev, dan amati perubahannya. - Sedang, Buat
lib/env.jsseperti contoh, lalu sengaja kosongkanSESSION_SECRET. Catat pesan error yang muncul dan jelaskan kenapa gagal cepat lebih baik daripada gagal diam-diam. - Menantang, Buat Route Handler
GET /api/configyang mengembalikan konfigurasi publik (nama situs, URL, versi aplikasi daripackage.json) untuk aplikasi mobile, tanpa membocorkan variabel rahasia. Tulis juga satu tes kecil yang memastikan response tidak mengandung kataSECRET.
Lihat solusi
// 1. components/Footer.jsx
'use client'
export function Footer() {
return <footer>© {process.env.NEXT_PUBLIC_SITE_NAME}</footer>
}
// Setelah ganti nilai di .env.local, dev server perlu di-restart agar nilai baru terbaca.
// 2. Pesan yang muncul di terminal kira-kira:
// Environment variable tidak valid: { SESSION_SECRET: [ 'Invalid input: expected string, received undefined' ] }
// Error: Konfigurasi environment salah, cek .env.local atau pengaturan hosting
//
// Gagal cepat lebih baik: tanpa validasi, aplikasi tetap jalan tapi jose menandatangani token
// dengan key kosong atau login selalu gagal, dan kamu baru sadar setelah pengguna komplain.
// 3a. app/api/config/route.js
import pkg from '@/package.json'
export function GET() {
// Daftar eksplisit (allowlist), JANGAN kirim process.env utuh
return Response.json({
siteName: process.env.NEXT_PUBLIC_SITE_NAME ?? 'Blog',
siteUrl: process.env.NEXT_PUBLIC_SITE_URL,
version: pkg.version,
})
}
// 3b. __tests__/config-route.test.js
import { expect, test } from 'vitest'
import { GET } from '@/app/api/config/route'
test('config publik tidak membocorkan rahasia', async () => {
process.env.SESSION_SECRET = 'jangan-bocor'
const res = GET()
const text = JSON.stringify(await res.json())
expect(text).not.toContain('jangan-bocor')
expect(text).not.toMatch(/SECRET/i)
})
Ringkasan
- Simpan rahasia dan nilai per lingkungan di
.env.local(tidak di-commit) dan di pengaturan hosting, bukan di kode. - Variabel tanpa awalan hanya tersedia di server;
NEXT_PUBLIC_ditanam ke bundle browser saat build dan terlihat publik. - Urutan muat:
process.env→.env.$(NODE_ENV).local→.env.local→.env.$(NODE_ENV)→.env. - Tool di luar Next.js butuh
--env-file-if-existsatau@next/envuntuk membaca.env.local. next.config.mjsmengatur fitur seperticacheComponents,output, gambar remote, redirect, dan header.- Validasi env dengan Zod di file
server-onlyagar salah konfigurasi ketahuan sejak start.