In this comprehensive lesson, you'll master deployment strategies and DevOps practices for React applications. You'll learn how to deploy applications to production, implement CI/CD pipelines, manage environments, and ensure reliable, scalable deployments. From local development to production infrastructure, you'll build skills to deliver professional React applications.
React applications can be deployed using various strategies:
Static Site Generation (SSG): Pre-built static files for optimal performance Server-Side Rendering (SSR): Dynamic server rendering for SEO and performance Hybrid Deployment: Combination of static and dynamic rendering Edge Computing: Deploy to edge locations for global performance
Deployment Philosophy: Automate everything, monitor continuously, and iterate rapidly.
Vercel: Optimized for Next.js with zero-config deployment Netlify: Great for static sites with form handling and edge functions AWS: Full control with scalable infrastructure Docker: Containerized deployment for consistency across environments
Key Considerations: Performance, scalability, cost, team expertise, and specific requirements.
Vercel provides the most streamlined deployment experience for Next.js:
// vercel.json configuration
{
"version": 2,
"builds": [
{
"src": "package.json",
"use": "@vercel/next"
}
],
"routes": [
{
"src": "/(.*)",
"dest": "/$1"
}
],
"env": {
"NEXT_PUBLIC_API_URL": "@api-url"
}
}
// next.config.js for Vercel optimization
module.exports = {
output: 'export',
trailingSlash: true,
images: {
unoptimized: true
},
assetPrefix: process.env.NODE_ENV === 'production' ? 'https://cdn.example.com' : undefined
}
For static site hosting on platforms like Netlify or S3:
// next.config.js for static export
module.exports = {
output: 'export',
trailingSlash: true,
images: {
unoptimized: true
},
distDir: 'out'
}
// package.json scripts
{
"scripts": {
"build": "next build",
"export": "next export",
"deploy": "npm run build && npm run export"
}
}
// GitHub Actions for static deployment
name: Deploy to Netlify
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '18'
- run: npm ci
- run: npm run build
- run: npm run export
- uses: netlify/actions/cli@master
with:
args: deploy --dir=out --prod
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
Automated testing and deployment pipeline:
# .github/workflows/ci-cd.yml
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Run linting
run: npm run lint
- name: Type check
run: npm run type-check
- name: Build application
run: npm run build
- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: build-files
path: .next/
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Download build artifacts
uses: actions/download-artifact@v3
with:
name: build-files
- name: Deploy to Vercel
uses: amondnet/vercel-action@v20
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.ORG_ID }}
vercel-project-id: ${{ secrets.PROJECT_ID }}
vercel-args: '--prod'
Deploy to different environments based on branch:
# .github/workflows/deploy.yml
name: Deploy Application
on:
push:
branches: [main, develop, staging]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Determine environment
id: env
run: |
if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
echo "::set-output name=environment::production"
echo "::set-output name=vercel-args::--prod"
elif [[ "${{ github.ref }}" == "refs/heads/staging" ]]; then
echo "::set-output name=environment::staging"
echo "::set-output name=vercel-args::"
else
echo "::set-output name=environment::development"
echo "::set-output name=vercel-args::"
fi
- name: Deploy to ${{ steps.env.outputs.environment }}
uses: amondnet/vercel-action@v20
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.ORG_ID }}
vercel-project-id: ${{ secrets.PROJECT_ID }}
vercel-args: ${{ steps.env.outputs.vercel-args }}
alias: ${{ steps.env.outputs.environment }}-my-app.vercel.app
Manage different environments securely:
// .env.local (development)
NEXT_PUBLIC_API_URL=http://localhost:3001/api
NEXT_PUBLIC_WS_URL=ws://localhost:3001
DATABASE_URL=postgresql://user:pass@localhost:5432/devdb
STRIPE_SECRET_KEY=sk_test_...
// .env.production (production)
NEXT_PUBLIC_API_URL=https://api.example.com
NEXT_PUBLIC_WS_URL=wss://api.example.com
DATABASE_URL=postgresql://user:pass@prod-db:5432/proddb
STRIPE_SECRET_KEY=sk_live_...
// next.config.js environment handling
module.exports = {
env: {
CUSTOM_KEY: process.env.CUSTOM_KEY,
},
webpack: (config, { dev, isServer }) => {
if (!dev && !isServer) {
config.resolve.fallback = {
...config.resolve.fallback,
fs: false,
}
}
return config
}
}
// lib/config.ts
interface Config {
apiUrl: string
wsUrl: string
isDevelopment: boolean
isProduction: boolean
features: {
analytics: boolean
betaFeatures: boolean
}
}
const config: Config = {
apiUrl: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001/api',
wsUrl: process.env.NEXT_PUBLIC_WS_URL || 'ws://localhost:3001',
isDevelopment: process.env.NODE_ENV === 'development',
isProduction: process.env.NODE_ENV === 'production',
features: {
analytics: process.env.NEXT_PUBLIC_ENABLE_ANALYTICS === 'true',
betaFeatures: process.env.NEXT_PUBLIC_BETA_FEATURES === 'true',
}
}
export default config
// Usage in components
import config from '@/lib/config'
function FeatureFlag({ children, feature }: { children: React.ReactNode, feature: keyof Config['features'] }) {
if (!config.features[feature]) {
return null
}
return <>{children}</>
}
Optimize your application for production:
// next.config.js production optimizations
module.exports = {
// Enable compression
compress: true,
// Optimize images
images: {
domains: ['example.com', 'cdn.example.com'],
formats: ['image/webp', 'image/avif'],
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
},
// Optimize bundles
webpack: (config, { isServer }) => {
if (!isServer) {
config.optimization.splitChunks = {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
common: {
name: 'common',
minChunks: 2,
chunks: 'all',
enforce: true,
},
},
}
}
return config
},
// Enable experimental features
experimental: {
optimizeCss: true,
optimizePackageImports: ['@shadcn/ui', 'lucide-react'],
}
}
Implement monitoring for production applications:
// lib/monitoring.ts
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals'
function sendToAnalytics(metric: any) {
// Send to your analytics service
if (process.env.NEXT_PUBLIC_ANALYTICS_ENDPOINT) {
fetch(process.env.NEXT_PUBLIC_ANALYTICS_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(metric),
})
}
}
// Initialize monitoring
if (typeof window !== 'undefined') {
getCLS(sendToAnalytics)
getFID(sendToAnalytics)
getFCP(sendToAnalytics)
getLCP(sendToAnalytics)
getTTFB(sendToAnalytics)
}
// Error boundary for production
export class ErrorBoundary extends React.Component {
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
if (process.env.NODE_ENV === 'production') {
// Send error to monitoring service
fetch('/api/errors', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
error: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack,
timestamp: new Date().toISOString(),
}),
})
}
}
}
Implement security measures for production:
// next.config.js security
const securityHeaders = [
{
key: 'X-DNS-Prefetch-Control',
value: 'on'
},
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload'
},
{
key: 'X-XSS-Protection',
value: '1; mode=block'
},
{
key: 'X-Frame-Options',
value: 'DENY'
},
{
key: 'X-Content-Type-Options',
value: 'nosniff'
},
{
key: 'Referrer-Policy',
value: 'origin-when-cross-origin'
},
{
key: 'Content-Security-Policy',
value: "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:;"
}
]
module.exports = {
async headers() {
return [
{
source: '/(.*)',
headers: securityHeaders,
},
]
}
}
Secure your API endpoints:
// middleware.ts for API protection
import { NextRequest, NextResponse } from 'next/server'
export function middleware(request: NextRequest) {
// Rate limiting
const ip = request.ip || request.headers.get('x-forwarded-for')
const rateLimit = checkRateLimit(ip)
if (!rateLimit.allowed) {
return new NextResponse('Too Many Requests', { status: 429 })
}
// CORS configuration
const response = NextResponse.next()
if (request.nextUrl.pathname.startsWith('/api/')) {
response.headers.set('Access-Control-Allow-Origin', process.env.ALLOWED_ORIGINS || '*')
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization')
}
return response
}
export const config = {
matcher: '/api/:path*'
}
// API route security
export async function GET(request: NextRequest) {
const authHeader = request.headers.get('authorization')
if (!authHeader || !validateToken(authHeader)) {
return new NextResponse('Unauthorized', { status: 401 })
}
// Process request
const data = await fetchData()
return NextResponse.json(data)
}
Set up comprehensive monitoring:
// lib/logging.ts
type LogLevel = 'info' | 'warn' | 'error' | 'debug'
interface LogEntry {
level: LogLevel
message: string
timestamp: string
userId?: string
sessionId?: string
metadata?: Record<string, any>
}
class Logger {
private static instance: Logger
private logs: LogEntry[] = []
static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger()
}
return Logger.instance
}
log(level: LogLevel, message: string, metadata?: Record<string, any>) {
const entry: LogEntry = {
level,
message,
timestamp: new Date().toISOString(),
metadata,
}
this.logs.push(entry)
// Send to external service in production
if (process.env.NODE_ENV === 'production') {
this.sendToExternalService(entry)
}
// Console output for development
if (process.env.NODE_ENV === 'development') {
console[level](message, metadata)
}
}
private async sendToExternalService(entry: LogEntry) {
try {
await fetch('/api/logs', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(entry),
})
} catch (error) {
console.error('Failed to send log:', error)
}
}
}
export const logger = Logger.getInstance()
Implement health monitoring:
// pages/api/health.ts
export default async function handler(req: NextRequest, res: NextResponse) {
const health = {
status: 'ok',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
memory: process.memoryUsage(),
version: process.env.npm_package_version,
}
try {
// Check database connection
await checkDatabase()
health.database = 'connected'
} catch (error) {
health.status = 'error'
health.database = 'disconnected'
return NextResponse.json(health, { status: 503 })
}
// Check external services
const services = await checkExternalServices()
health.services = services
const allServicesHealthy = Object.values(services).every(status => status === 'ok')
if (!allServicesHealthy) {
health.status = 'degraded'
return NextResponse.json(health, { status: 200 })
}
return NextResponse.json(health)
}
In this comprehensive lesson, you've mastered deployment strategies and DevOps practices for React applications. You now understand:
In the final lesson, you'll apply all these concepts in a comprehensive final project that demonstrates your mastery of modern React development.