kodingup.camp

Beranda/Next.js Mastery: Full-Stack, Auth & Deploy

Environment & Konfigurasi

Modul 7 dari 115 menit bacaEnvironment Variables and Config

Isi modul

    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):

    1. process.env (variabel yang sudah diset di sistem atau oleh hosting)
    2. .env.$(NODE_ENV).local (misalnya .env.development.local)
    3. .env.local (tidak dibaca saat NODE_ENV=test)
    4. .env.$(NODE_ENV) (misalnya .env.production)
    5. .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:

    1. Nilainya terlihat publik di file JavaScript yang diunduh browser. Jangan pernah memberi awalan NEXT_PUBLIC_ pada rahasia.
    2. 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 seperti process.env[nama] atau const env = process.env; env.NEXT_PUBLIC_X tidak 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:

    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

    1. 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.
    2. 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.
    3. Mengubah NEXT_PUBLIC_ di hosting tanpa build ulang. Nilainya ditanam saat build. Redeploy setelah mengubah variabel publik.
    4. Membaca env dengan nama dinamis di client. process.env[key] tidak diganti saat build dan hasilnya undefined. Tulis nama lengkapnya secara langsung.
    5. Script seed atau drizzle-kit tidak menemukan variabel. Tool di luar Next.js tidak membaca .env.local otomatis. Pakai --env-file-if-exists atau loadEnvConfig dari @next/env.

    Latihan

    1. Mudah, Tambahkan NEXT_PUBLIC_SITE_NAME=Blog Kodingup di .env.local, lalu tampilkan di footer lewat Client Component. Ubah nilainya, restart npm run dev, dan amati perubahannya.
    2. Sedang, Buat lib/env.js seperti contoh, lalu sengaja kosongkan SESSION_SECRET. Catat pesan error yang muncul dan jelaskan kenapa gagal cepat lebih baik daripada gagal diam-diam.
    3. Menantang, Buat Route Handler GET /api/config yang mengembalikan konfigurasi publik (nama situs, URL, versi aplikasi dari package.json) untuk aplikasi mobile, tanpa membocorkan variabel rahasia. Tulis juga satu tes kecil yang memastikan response tidak mengandung kata SECRET.
    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

    Video pendamping

    Kalau penjelasan di atas belum masuk, tonton ini. Video dari kanal YouTube pihak lain.

    Keeping Your Data Secure: Environment Variables in NextJS / ReactJosh tried coding · Bahasa InggrisBuka di YouTube

    Kuis pemahaman

    Pilih satu jawaban per soal. Penjelasan muncul setelah kamu menjawab.