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.htmladalah kunci SPA — fallback ke index.html untuk semua path yang tidak ada sebagai file fisik.index.htmljangan 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 --deleteuntuk deployment — file lama dengan hash berbeda otomatis terhapus, tidak perlu reload Nginx.