SPA React/Vue

SPA React/Vue #

Single Page Application (SPA) seperti React, Vue, atau Angular punya satu tantangan spesifik saat di-deploy: semua routing dikelola oleh JavaScript di browser, bukan oleh server. Jika pengguna langsung mengakses URL seperti /dashboard/settings, server perlu mengembalikan index.html — bukan 404 — agar router JavaScript bisa mengambil alih.

Masalah: Client-Side Routing vs Server #

Pengguna buka browser, ketik: https://app.com/products/123

Tanpa konfigurasi yang benar:
  Nginx cari file /var/www/app/products/123
  File tidak ada → 404 Not Found ✗

Dengan konfigurasi yang benar:
  Nginx cari file /var/www/app/products/123
  Tidak ada → fallback ke /var/www/app/index.html ✓
  Browser muat index.html + bundle JS
  React Router / Vue Router baca URL → tampilkan halaman products/123 ✓

try_files $uri $uri/ /index.html adalah solusinya — jika file atau direktori tidak ditemukan, kembalikan index.html.


Konfigurasi Dasar SPA #

server {
    listen 443 ssl;
    http2 on;
    server_name app.com;

    ssl_certificate     /etc/letsencrypt/live/app.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/app.com/privkey.pem;

    root /var/www/app/dist;   # Direktori hasil build (dist/ atau build/)
    index index.html;

    # Gzip untuk bundle JS/CSS yang besar
    gzip on;
    gzip_types text/html text/css application/javascript application/json;
    gzip_min_length 1024;

    location / {
        # Kunci utama SPA: fallback ke index.html jika file tidak ada
        try_files $uri $uri/ /index.html;
    }
}

server {
    listen 80;
    server_name app.com;
    return 301 https://$host$request_uri;
}

Cache Strategy: index.html vs Aset #

Ini bagian yang paling kritis dan paling sering salah. Ada dua jenis file dengan kebutuhan cache yang berbeda:

index.html — harus selalu fresh. Ini yang me-load bundle JS/CSS terbaru. Jika di-cache terlalu lama, pengguna mungkin masih menggunakan HTML lama yang merujuk ke bundle yang sudah tidak ada setelah deployment baru.

Bundle JS/CSS/gambar — bisa di-cache sangat lama. Build tools modern (Vite, Webpack, Create React App) menambahkan hash konten ke nama file: app.a3f8b2c.js. Jika konten berubah, nama file berubah — cache lama otomatis tidak dipakai.

server {
    root /var/www/app/dist;

    # index.html: JANGAN cache atau cache sangat singkat
    # Setiap kali ada deployment baru, browser harus ambil HTML terbaru
    location = /index.html {
        add_header Cache-Control "no-cache, no-store, must-revalidate";
        add_header Pragma "no-cache";
        add_header Expires "0";
    }

    # Aset dengan hash di nama file: cache 1 tahun
    # Vite/Webpack menghasilkan nama seperti: main.abc123.js
    location ~* \.(js|css)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        access_log off;
    }

    # Gambar, font, favicon
    location ~* \.(png|jpg|jpeg|gif|webp|svg|ico|woff2|woff|ttf)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        access_log off;
    }

    # Semua request lain: fallback ke index.html
    location / {
        try_files $uri $uri/ /index.html;
        add_header Cache-Control "no-cache";
    }
}

SPA + API Backend di Domain yang Sama #

Pola umum: SPA dilayani dari /, API dari /api/. Menghindari masalah CORS:

server {
    listen 443 ssl;
    http2 on;
    server_name app.com;

    ssl_certificate /etc/letsencrypt/live/app.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/app.com/privkey.pem;

    root /var/www/app/dist;
    gzip on;
    gzip_types text/html application/javascript application/json text/css;

    # API backend — jangan fallback ke index.html
    location /api/ {
        proxy_pass http://localhost:8080;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # Static assets — cache agresif
    location ~* \.(js|css|png|jpg|svg|ico|woff2)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        access_log off;
        try_files $uri =404;
    }

    # index.html — no-cache
    location = /index.html {
        add_header Cache-Control "no-cache, must-revalidate";
    }

    # SPA routing — fallback ke index.html
    location / {
        try_files $uri $uri/ /index.html;
    }
}

Perhatikan urutan location: /api/ harus sebelum / agar request ke /api/users tidak jatuh ke try_files SPA.


Deployment: Update SPA Tanpa Downtime #

# Build SPA
npm run build
# Menghasilkan dist/ dengan file baru bernama app.NEWHASH.js

# Deploy — copy ke server
rsync -av --delete dist/ /var/www/app/dist/

# Nginx tidak perlu di-reload!
# File baru sudah tersedia di disk
# Browser lama masih menggunakan index.html lama dan bundle lama (dari cache mereka)
# Browser yang request index.html baru mendapatkan HTML terbaru yang merujuk bundle baru

Karena nama file aset menyertakan hash, deployment baru tidak akan “merusak” browser yang sedang menggunakan versi lama — mereka tetap bisa load bundle lama (yang masih ada di disk karena rsync --delete hanya menghapus yang sudah tidak ada di source).


Security Headers untuk SPA #

server {
    # Content Security Policy untuk SPA
    # Sesuaikan dengan CDN dan domain yang digunakan
    add_header Content-Security-Policy
        "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://api.example.com; font-src 'self' https://fonts.gstatic.com;"
        always;

    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
}

'unsafe-inline' untuk script dan style sering dibutuhkan karena banyak SPA framework menggunakan inline style atau script injection. Untuk keamanan lebih ketat, gunakan CSP nonce atau hash — tapi ini butuh dukungan dari framework.


Ringkasan #

  • try_files $uri $uri/ /index.html adalah kunci SPA — fallback ke index.html untuk semua path yang tidak ada sebagai file fisik.
  • index.html jangan di-cache (no-cache) — harus selalu fresh agar deployment baru langsung diambil browser.
  • Bundle JS/CSS dengan hash di nama file bisa di-cache sangat lama (1y + immutable) — nama file otomatis berubah saat konten berubah.
  • Letakkan location /api/ sebelum location / agar request API tidak jatuh ke SPA routing.
  • Gunakan rsync --delete untuk deployment — file lama dengan hash berbeda otomatis terhapus, tidak perlu reload Nginx.

← Sebelumnya: WebSocket   Berikutnya: Error Umum →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact