SPA Deployment β Deploying Single-Page Applications to Production
In this tutorial, you will learn about SPA Deployment. We cover key concepts, practical examples, and best practices to help you master this topic.
SPA deployment covers hosting strategies, CDN configuration, build optimization, environment variable management, CI/CD pipelines, and deploying to Netlify, Vercel, AWS S3 with CloudFront, and custom servers.
What You'll Learn
By the end of this tutorial, you will understand how to deploy SPAs to production including build optimization for deployment, CDN configuration for global performance, environment management across staging and production, CI/CD pipeline setup, and deployment to multiple hosting platforms.
Why It Matters
A perfectly built SPA is useless if it is not deployed correctly. Misconfigured deployments cause blank pages on route refresh (404 errors for client-side routes), slow load times from improper caching, and security issues from exposed environment variables. Proper deployment is the final step that makes your app accessible to users.
Real-World Use
A React SPA deployed to AWS S3 with CloudFront initially served 404 errors on all routes except the home page. Users could not bookmark or refresh any page. After configuring error page redirects to index.html with a 200 status, all routes worked correctly. Page load time dropped from 4.2s to 1.1s after enabling CloudFront caching and Brotli compression.
SPA Deployment Architecture
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β SPA Production Architecture β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β ββββββββββββ ββββββββββββ ββββββββββββββββββββ β
β β Developer ββββ>β CI/CD ββββ>β Build Artifact β β
β β (git push)β β Pipeline β β (dist folder) β β
β ββββββββββββ ββββββββββββ ββββββββββ¬ββββββββββ β
β β β
β ββββββββββββββββββββββββββββββββββββββββββΌβββββββββββ β
β β CDN / Static Hosting β β
β β ββββββββββ ββββββββββ ββββββββββ ββββββββββ β β
β β β Netlifyβ β Vercel β β S3 + β β Nginx β β β
β β β β β β β Cloud β β Server β β β
β β β β β β β Front β β β β β
β β ββββββββββ ββββββββββ ββββββββββ ββββββββββ β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Critical Configurations β β
β β β’ Index document: index.html β β
β β β’ Error document: index.html (for SPA routing) β β
β β β’ Cache headers: immutable for hashed assets β β
β β β’ Compression: Brotli / Gzip β β
β β β’ HTTPS: Enforce TLS 1.3 β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββ β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Think of deploying an SPA like moving into a new house. The build process is packing your belongings (bundling files). The CDN is the moving truck that delivers everything. The configuration (error page redirects, cache headers) is like setting up the utilities β without them, the house looks nice but nothing works. The CI/CD pipeline is a moving crew that packs the same way every time.
SPA Route Handling Configuration
// Netlify configuration β _redirects file
// This ensures client-side routes work on refresh
/*
/* /index.html 200
/api/* https://api.example.com/:splat 200
# Cache headers for static assets
/static/* Cache-Control: public, max-age=31536000, immutable
/assets/* Cache-Control: public, max-age=31536000, immutable
/*.js Cache-Control: public, max-age=31536000, immutable
/*.css Cache-Control: public, max-age=31536000, immutable
/*.svg Cache-Control: public, max-age=31536000, immutable
/*.png Cache-Control: public, max-age=31536000, immutable
# No cache for index.html
/index.html Cache-Control: public, max-age=0, must-revalidate
*/
// Nginx configuration for SPA
/*
server {
listen 443 ssl;
server_name example.com;
root /var/www/spa;
index index.html;
# SPA route handling β serve index.html for all routes
location / {
try_files $uri $uri/ /index.html;
}
# Cache static assets forever (hashed filenames)
location /static/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Proxy API requests
location /api/ {
proxy_pass https://api.example.com/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# Security headers
add_header X-Frame-Options "DENY";
add_header X-Content-Type-Options "nosniff";
add_header Referrer-Policy "strict-origin-when-cross-origin";
}
*/
CI/CD Pipeline Configuration
# GitHub Actions workflow for SPA deployment
name: Deploy SPA
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint and type check
run: |
npm run lint
npm run typecheck
- name: Run tests
run: npm test -- --coverage
- name: Build
run: npm run build
env:
VITE_API_URL: https://api.example.com
VITE_SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
VITE_GA_ID: ${{ secrets.GA_ID }}
- name: Deploy to Netlify
uses: nwtgck/actions-netlify@v3
with:
publish-dir: ./dist
production-branch: main
deploy-message: "Deploy from GitHub Actions"
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
Environment Management
// Vite environment variables
// .env.production
VITE_API_URL=https://api.example.com
VITE_SENTRY_DSN=https://xxx@sentry.io/yyy
VITE_GA_ID=G-XXXXXXXXXX
VITE_ENVIRONMENT=production
// .env.staging
VITE_API_URL=https://staging-api.example.com
VITE_SENTRY_DSN=https://xxx@sentry.io/yyy
VITE_GA_ID=G-YYYYYYYYYY
VITE_ENVIRONMENT=staging
// Usage in code
const API_URL = import.meta.env.VITE_API_URL;
const isProduction = import.meta.env.VITE_ENVIRONMENT === 'production';
// Validate required env vars at build time
function validateEnv() {
const required = ['VITE_API_URL'];
const missing = required.filter(key => !import.meta.env[key]);
if (missing.length > 0) {
throw new Error(
`Missing required environment variables: ${missing.join(', ')}`
);
}
}
validateEnv();
Build Optimization for Deployment
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
plugins: [
react(),
visualizer({
filename: 'dist/stats.html',
open: false,
gzipSize: true,
brotliSize: true
})
],
build: {
outDir: 'dist',
sourcemap: false,
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
},
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom', 'react-router-dom'],
ui: ['@mui/material', '@emotion/react'],
charts: ['recharts'],
utils: ['date-fns', 'axios', 'zustand']
}
}
},
// Enable Brotli compression
reportCompressedSize: true,
// Chunk size warnings
chunkSizeWarningLimit: 200
}
});
Common Mistakes
- No SPA route fallback. Deploying an SPA without configuring the server to serve index.html for all routes results in 404 errors when users refresh or directly access any route other than the root.
- Caching index.html. If index.html is cached by the CDN with a long TTL, users never receive updated versions of the app. Always set Cache-Control: no-cache for index.html.
- Exposing secrets in client-side code. Environment variables prefixed with VITE_ are inlined into the bundle at build time and visible in the browser. Never include API keys for services that run server-side.
- Skipping the build step in CI. Building locally and committing the dist folder leads to inconsistent builds and bloated repositories. Always build in CI from a clean environment.
- No staging environment. Deploying directly to production without testing on a staging environment increases the risk of breaking changes reaching users. Always deploy to staging first.
Practice Questions
- Why does an SPA return a 404 error when the user refreshes a client-side route, and how do you fix it?
- What cache headers should you set for hashed static assets versus index.html?
- Why should environment variables be validated at build time?
- What is the purpose of a staging environment in the deployment pipeline?
- How do manual chunks improve caching of vendor libraries?
Challenge: Set up a complete deployment pipeline for an SPA: Vite configuration with manual chunks and minification, Netlify deployment with _redirects for SPA routing, GitHub Actions CI/CD workflow that runs lint, tests, and Type Checking before building, staging and production environments with different API URLs, and verify that the deployed app passes all routes with 200 status.
FAQ
Mini Project
Deploy a complete SPA to production: set up a Vite-based React SPA with route-based Code Splitting, configure Netlify deployment with SPA route handling (_redirects), set up GitHub Actions CI/CD with lint, test, build, and deploy steps, configure staging and production environments, add cache headers for static assets, and verify the deployment with Lighthouse and curl tests.
What's Next
You understand SPA deployment. Now build a complete SPA mini project that combines everything you have learned.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro