6.4: Full-Stack Deployment & Production Best Practices
Optimize your website performance for faster loading times and better user experience. Learn techniques for image optimization, code minification, caching strategies, and using performance tools to measure and improve speed.
1. Full-Stack Architecture Overview
Production Architecture
User's Browser
↓
CDN (Cloudflare, Netlify Edge)
↓
Frontend (Vercel/Netlify)
↓ API calls
Backend API (Railway/Render)
↓
Database (PostgreSQL)
↓
External APIs (if any)
Separation of Concerns
Why separate frontend and backend?
Benefits:
- Independent scaling
- Technology flexibility
- Easier maintenance
- Better security
- Multiple frontends (web, mobile) can use same API
Deployment strategy:
- Frontend: Static hosting (Vercel, Netlify)
- Backend: Application hosting (Railway, Render)
- Database: Managed database service
2. Environment Configuration
Environment Types
1. Development (local):
- Local database
- Debug mode enabled
- Hot reload
- Detailed error messages
2. Staging (optional):
- Production-like environment
- Test deployment
- Pre-release testing
3. Production:
- Live users
- Optimized builds
- Error tracking
- Performance monitoring
Frontend Environment Variables
Create multiple .env files:
.env.development:
VITE_API_URL=http://localhost:8000
VITE_ENVIRONMENT=development
VITE_ENABLE_DEBUG=true
.env.production:
VITE_API_URL=https://api.myapp.com
VITE_ENVIRONMENT=production
VITE_ENABLE_DEBUG=false
Usage in Vue:
// src/config.js
export const config = {
apiUrl: import.meta.env.VITE_API_URL,
environment: import.meta.env.VITE_ENVIRONMENT,
isProduction: import.meta.env.VITE_ENVIRONMENT === 'production',
isDevelopment: import.meta.env.VITE_ENVIRONMENT === 'development'
}
// Use in components
import { config } from '@/config'
if (config.isDevelopment) {
console.log('Debug info:', data)
}
Backend Environment Variables
Create .env file:
# .env (local development)
DATABASE_URL=sqlite:///./local.db
SECRET_KEY=dev-secret-key-change-in-production
DEBUG=true
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173
ENVIRONMENT=development
Production environment (set in Railway/Render):
DATABASE_URL=postgresql://user:pass@host:5432/dbname
SECRET_KEY=super-secret-production-key-very-long
DEBUG=false
ALLOWED_ORIGINS=https://myapp.com,https://www.myapp.com
ENVIRONMENT=production
Load in FastAPI:
# config.py
from pydantic import BaseSettings
from typing import List
class Settings(BaseSettings):
database_url: str
secret_key: str
debug: bool = False
allowed_origins: List[str]
environment: str = "development"
class Config:
env_file = ".env"
settings = Settings()
# main.py
from config import settings
app = FastAPI(debug=settings.debug)
app.add_middleware(
CORSMiddleware,
allow_origins=settings.allowed_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
3. Error Handling and Logging
Frontend Error Handling
Global error handler:
// src/services/api.js
import axios from 'axios'
import { config } from '@/config'
const apiClient = axios.create({
baseURL: config.apiUrl,
headers: {
'Content-Type': 'application/json'
}
})
// Request interceptor
apiClient.interceptors.request.use(
(config) => {
// Add auth token if available
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)
// Response interceptor
apiClient.interceptors.response.use(
(response) => response,
(error) => {
if (error.response) {
// Server responded with error
const status = error.response.status
const message = error.response.data.detail || 'An error occurred'
if (status === 401) {
// Unauthorized - redirect to login
localStorage.removeItem('token')
window.location.href = '/login'
} else if (status === 404) {
console.error('Resource not found')
} else if (status >= 500) {
console.error('Server error:', message)
}
return Promise.reject(new Error(message))
} else if (error.request) {
// Request made but no response
return Promise.reject(new Error('Network error - server not responding'))
} else {
// Request setup error
return Promise.reject(error)
}
}
)
export default apiClient
Component error handling:
<script setup>
import { ref } from 'vue'
import api from '@/services/api'
const data = ref([])
const loading = ref(false)
const error = ref(null)
async function fetchData() {
loading.value = true
error.value = null
try {
const response = await api.get('/data')
data.value = response.data
} catch (err) {
error.value = err.message
// Log to error tracking service
if (window.Sentry) {
window.Sentry.captureException(err)
}
} finally {
loading.value = false
}
}
</script>
<template>
<div>
<div v-if="error" class="error-banner">
{{ error }}
<button @click="fetchData">Retry</button>
</div>
<div v-if="loading">Loading...</div>
<div v-else>{{ data }}</div>
</div>
</template>
Backend Error Handling
Custom exception handlers:
# main.py
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse
import logging
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
app = FastAPI()
# Global exception handler
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
logger.error(f"Unhandled exception: {exc}", exc_info=True)
return JSONResponse(
status_code=500,
content={"detail": "Internal server error"}
)
# HTTP exception handler
@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
logger.warning(f"HTTP {exc.status_code}: {exc.detail}")
return JSONResponse(
status_code=exc.status_code,
content={"detail": exc.detail}
)
# Validation error handler
from fastapi.exceptions import RequestValidationError
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
logger.warning(f"Validation error: {exc}")
return JSONResponse(
status_code=422,
content={"detail": exc.errors()}
)
Structured logging:
import logging
from datetime import datetime
logger = logging.getLogger(__name__)
@app.post("/users")
async def create_user(user: UserCreate):
logger.info(f"Creating user: {user.email}")
try:
db_user = create_user_in_db(user)
logger.info(f"User created successfully: {db_user.id}")
return db_user
except Exception as e:
logger.error(f"Failed to create user: {e}", exc_info=True)
raise HTTPException(status_code=500, detail="User creation failed")
4. Performance Optimization
Frontend Optimization
1. Code Splitting:
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue') // Lazy load
},
{
path: '/dashboard',
name: 'Dashboard',
component: () => import('@/views/Dashboard.vue')
}
]
})
2. Image Optimization:
<template>
<!-- Use modern formats -->
<picture>
<source srcset="image.webp" type="image/webp">
<source srcset="image.jpg" type="image/jpeg">
<img src="image.jpg" alt="Description" loading="lazy">
</picture>
</template>
3. Caching API Responses:
// Simple cache implementation
const cache = new Map()
async function fetchWithCache(key, fetchFn, ttl = 60000) {
const cached = cache.get(key)
if (cached && Date.now() - cached.timestamp < ttl) {
return cached.data
}
const data = await fetchFn()
cache.set(key, { data, timestamp: Date.now() })
return data
}
// Usage
const users = await fetchWithCache('users', () => api.getUsers())
4. Debouncing Search:
<script setup>
import { ref, watch } from 'vue'
import { debounce } from 'lodash-es'
const searchQuery = ref('')
const results = ref([])
const search = debounce(async (query) => {
if (!query) return
results.value = await api.search(query)
}, 300)
watch(searchQuery, (newQuery) => {
search(newQuery)
})
</script>
<template>
<input v-model="searchQuery" placeholder="Search...">
<div v-for="result in results" :key="result.id">
{{ result.name }}
</div>
</template>
Backend Optimization
1. Database Query Optimization:
from sqlalchemy.orm import joinedload
# Bad: N+1 queries
users = db.query(User).all()
for user in users:
print(user.posts) # Each user triggers a separate query
# Good: Eager loading
users = db.query(User).options(joinedload(User.posts)).all()
for user in users:
print(user.posts) # All posts loaded in one query
2. Caching with Redis:
import redis
from functools import wraps
import json
redis_client = redis.Redis(host='localhost', port=6379, db=0)
def cache(key_prefix: str, ttl: int = 300):
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
cache_key = f"{key_prefix}:{args}:{kwargs}"
cached = redis_client.get(cache_key)
if cached:
return json.loads(cached)
result = await func(*args, **kwargs)
redis_client.setex(cache_key, ttl, json.dumps(result))
return result
return wrapper
return decorator
@app.get("/users/{user_id}")
@cache("user", ttl=60)
async def get_user(user_id: int):
return db.query(User).filter(User.id == user_id).first()
3. Async Database Queries:
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
engine = create_async_engine("postgresql+asyncpg://...")
@app.get("/users")
async def get_users(db: AsyncSession = Depends(get_async_db)):
result = await db.execute(select(User))
return result.scalars().all()
4. Response Compression:
from fastapi.middleware.gzip import GZipMiddleware
app.add_middleware(GZipMiddleware, minimum_size=1000)
5. Security Best Practices
Frontend Security
1. XSS Prevention:
<!-- Vue automatically escapes content -->
<div>{{ userInput }}</div> <!-- Safe -->
<!-- Be careful with v-html -->
<div v-html="sanitizedHtml"></div> <!-- Use DOMPurify to sanitize -->
import DOMPurify from 'dompurify'
const clean = DOMPurify.sanitize(dirtyHtml)
2. Secure Storage:
// Don't store sensitive data in localStorage
// Use httpOnly cookies for tokens (set by backend)
// If you must use localStorage:
// Encrypt sensitive data
import CryptoJS from 'crypto-js'
function secureStore(key, value) {
const encrypted = CryptoJS.AES.encrypt(
JSON.stringify(value),
'encryption-key'
).toString()
localStorage.setItem(key, encrypted)
}
3. CSRF Protection:
// For APIs, use token-based auth (JWT)
// For cookies, ensure backend sets:
// - SameSite=Strict
// - HttpOnly
// - Secure (HTTPS only)
Backend Security
1. Input Validation:
from pydantic import BaseModel, validator, EmailStr
class UserCreate(BaseModel):
email: EmailStr
password: str
age: int
@validator('password')
def password_strength(cls, v):
if len(v) < 8:
raise ValueError('Password must be at least 8 characters')
if not any(c.isupper() for c in v):
raise ValueError('Password must contain uppercase letter')
return v
@validator('age')
def valid_age(cls, v):
if v < 18 or v > 120:
raise ValueError('Age must be between 18 and 120')
return v
2. Rate Limiting:
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
@app.post("/login")
@limiter.limit("5/minute")
async def login(request: Request, credentials: LoginCredentials):
# Login logic
pass
3. SQL Injection Prevention:
# SQLAlchemy ORM prevents SQL injection automatically
# Bad (raw SQL):
db.execute(f"SELECT * FROM users WHERE email = '{email}'")
# Good (parameterized):
db.query(User).filter(User.email == email).first()
4. Security Headers:
from fastapi.middleware.trustedhost import TrustedHostMiddleware
app.add_middleware(
TrustedHostMiddleware,
allowed_hosts=["myapp.com", "*.myapp.com"]
)
@app.middleware("http")
async def add_security_headers(request: Request, call_next):
response = await call_next(request)
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["X-XSS-Protection"] = "1; mode=block"
response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
return response
5. Secrets Management:
# Never hardcode secrets
# Bad:
SECRET_KEY = "my-secret-key"
# Good:
import os
SECRET_KEY = os.getenv("SECRET_KEY")
if not SECRET_KEY:
raise ValueError("SECRET_KEY environment variable not set")
6. Monitoring and Health Checks
Health Check Endpoint
# main.py
from datetime import datetime
@app.get("/health")
async def health_check():
return {
"status": "healthy",
"timestamp": datetime.utcnow().isoformat(),
"environment": settings.environment,
"version": "1.0.0"
}
@app.get("/health/db")
async def database_health(db: Session = Depends(get_db)):
try:
# Test database connection
db.execute("SELECT 1")
return {"status": "healthy", "database": "connected"}
except Exception as e:
logger.error(f"Database health check failed: {e}")
return JSONResponse(
status_code=503,
content={"status": "unhealthy", "database": "disconnected"}
)
Error Tracking with Sentry
Install Sentry:
# Frontend
npm install @sentry/vue
# Backend
pip install sentry-sdk[fastapi]
Frontend setup:
// main.js
import { createApp } from 'vue'
import * as Sentry from '@sentry/vue'
import App from './App.vue'
const app = createApp(App)
if (import.meta.env.VITE_ENVIRONMENT === 'production') {
Sentry.init({
app,
dsn: import.meta.env.VITE_SENTRY_DSN,
environment: import.meta.env.VITE_ENVIRONMENT,
tracesSampleRate: 1.0
})
}
app.mount('#app')
Backend setup:
# main.py
import sentry_sdk
from sentry_sdk.integrations.fastapi import FastApiIntegration
if settings.environment == "production":
sentry_sdk.init(
dsn=settings.sentry_dsn,
integrations=[FastApiIntegration()],
traces_sample_rate=1.0,
environment=settings.environment
)
Application Monitoring
1. Uptime Monitoring:
- UptimeRobot (free)
- Pingdom
- Better Uptime
2. Performance Monitoring:
- Vercel Analytics (built-in)
- Google Lighthouse
- WebPageTest
3. Log Aggregation:
- LogTail
- Papertrail
- Datadog
7. CI/CD Pipeline
GitHub Actions for Frontend
Create .github/workflows/deploy.yml:
name: Deploy Frontend
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Build project
run: npm run build
env:
VITE_API_URL: ${{ secrets.VITE_API_URL }}
- name: Deploy to Netlify
uses: netlify/actions/cli@master
with:
args: deploy --prod
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
GitHub Actions for Backend
name: Deploy Backend
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install pytest
- name: Run tests
run: pytest
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Deploy to Railway
run: |
npm i -g @railway/cli
railway up
env:
RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
8. Database Migration and Backups
Database Migrations with Alembic
Install Alembic:
pip install alembic
Initialize:
alembic init migrations
Create migration:
alembic revision --autogenerate -m "Add users table"
Apply migration:
alembic upgrade head
Production deployment:
# In your startup script or Dockerfile
alembic upgrade head && uvicorn main:app
Database Backups
PostgreSQL backup script:
#!/bin/bash
# backup.sh
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/backups"
DB_NAME="myapp"
pg_dump $DATABASE_URL > $BACKUP_DIR/backup_$DATE.sql
# Upload to S3 (optional)
aws s3 cp $BACKUP_DIR/backup_$DATE.sql s3://my-backups/
# Keep only last 7 days
find $BACKUP_DIR -name "backup_*.sql" -mtime +7 -delete
Automated backups:
- Railway: Built-in daily backups
- Render: Automatic backups on paid plans
- Self-hosted: Use cron jobs
9. Scaling Strategies
Horizontal Scaling
Load balancing:
Load Balancer
↓
┌────────────┼────────────┐
↓ ↓ ↓
Server 1 Server 2 Server 3
Stateless backend:
# Don't store state in memory
# Bad:
user_sessions = {} # Lost when server restarts
# Good: Use Redis or database
import redis
redis_client = redis.Redis()
Vertical Scaling
Increase resources:
- More CPU
- More RAM
- Faster database
When to scale vertically:
- Single bottleneck (database)
- Simple architecture
- Cost-effective for small apps
Caching Strategy
Multi-level caching:
Browser Cache (5 min)
↓
CDN Cache (1 hour)
↓
Redis Cache (5 min)
↓
Database
10. Deployment Checklist
Pre-Deployment
- All tests passing
- Environment variables configured
- Database migrations ready
- Dependencies updated
- Security audit completed
- Performance tested
- Error tracking configured
- Monitoring set up
- Backup strategy in place
- Rollback plan prepared
Deployment
- Deploy to staging first
- Test staging thoroughly
- Run database migrations
- Deploy backend
- Deploy frontend
- Verify health checks
- Test critical paths
- Monitor error logs
Post-Deployment
- Verify all features work
- Check performance metrics
- Monitor error rates
- Check database performance
- Verify backups working
- Update documentation
- Notify team/users
11. Troubleshooting Production Issues
Common Issues and Solutions
Issue 1: API 500 errors
Debug:
# Check logs
logger.error(f"Error details: {e}", exc_info=True)
# Add detailed error response (dev only)
if settings.debug:
return {"error": str(e), "traceback": traceback.format_exc()}
Issue 2: Database connection pool exhausted
Solution:
# Configure connection pooling
engine = create_engine(
settings.database_url,
pool_size=20,
max_overflow=0,
pool_pre_ping=True
)
Issue 3: CORS errors in production
Debug:
// Check browser console
// Verify backend CORS config
// Ensure frontend URL in allowed origins
Issue 4: Environment variables not loading
Check:
# Railway/Render dashboard
# Verify variable names match
# Redeploy after adding variables
Debugging Tools
1. Browser DevTools:
- Network tab (API calls)
- Console (errors)
- Application tab (storage)
2. Backend logs:
# Railway
railway logs
# Render
# View in dashboard
# Docker
docker logs container-name
3. Database queries:
# Log all SQL queries (development only)
import logging
logging.basicConfig()
logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)
12. Knowledge Check
Question 1: What's the difference between horizontal and vertical scaling?
Show answer
Horizontal scaling adds more servers (scale out), while vertical scaling increases resources on existing servers (scale up). Horizontal scaling provides better redundancy and fault tolerance.Question 2: Why use separate environment variables for dev and production?
Show answer
To use different configurations (local vs production databases, debug modes, API keys) without changing code, keeping development isolated from production.Question 3: What should a health check endpoint return?
Show answer
Status (healthy/unhealthy), timestamp, version, and optionally database/service status. Should return 200 for healthy, 503 for unhealthy.Question 4: How do you prevent SQL injection in FastAPI?
Show answer
Use SQLAlchemy ORM which automatically parameterizes queries. Never concatenate user input directly into SQL strings.Question 5: What's the purpose of error tracking services like Sentry?
Show answer
To automatically capture, aggregate, and alert on production errors, providing context like stack traces, user data, and breadcrumbs for debugging.13. Practical Exercises
Exercise 6.4.1: Deploy Full Stack App
- Create a complete Todo app (Vue + FastAPI)
- Set up separate dev and production environments
- Deploy frontend to Vercel
- Deploy backend to Railway
- Configure CORS and environment variables
- Test the full application
Exercise 6.4.2: Implement Monitoring
- Add Sentry to both frontend and backend
- Create health check endpoints
- Set up UptimeRobot monitoring
- Trigger test errors and verify tracking
- Create error handling for network failures
Exercise 6.4.3: Optimize Performance
- Add caching to API responses
- Implement lazy loading in Vue
- Optimize database queries
- Add loading states
- Measure before/after performance
Exercise 6.4.4: Set Up CI/CD
- Create GitHub Actions workflow
- Add automated tests
- Configure automatic deployment
- Test by pushing changes
- Verify automatic deployment works
Exercise 6.4.5: Security Audit
- Add input validation
- Implement rate limiting
- Add security headers
- Test for common vulnerabilities
- Enable HTTPS everywhere
14. Key Takeaways
- Separate environments (dev, staging, production) with appropriate configurations
- Environment variables manage secrets and configuration
- Error handling and logging are critical for debugging production issues
- Performance optimization includes caching, lazy loading, and query optimization
- Security requires input validation, rate limiting, and secure headers
- Monitoring enables proactive issue detection
- CI/CD automates testing and deployment
- Health checks verify application status
- Scaling can be horizontal (more servers) or vertical (bigger servers)
- Backups and rollback plans prevent data loss
15. Course Completion
Congratulations! You've completed Phase 6: Deployment!
You now have the skills to:
- Deploy full-stack web applications
- Manage production environments
- Implement best practices for security and performance
- Monitor and troubleshoot production issues
- Scale applications as they grow
Next Steps in Your Journey
1. Build Your Capstone Project:
- Choose a real-world problem to solve
- Apply everything you've learned
- Deploy to production
- Share with the world!
2. Continue Learning:
- GraphQL APIs
- WebSockets for real-time features
- Advanced Vue patterns
- Docker and containers
- Kubernetes for orchestration
- Microservices architecture
3. Join the Community:
- Contribute to open source
- Share your projects
- Help other learners
- Build your portfolio
4. Keep Building:
- Personal projects
- Freelance work
- Contribute to company projects
- Launch your own products
16. Final Project Ideas
1. Portfolio Website with CMS:
- Vue frontend
- FastAPI backend
- Admin panel to manage content
- Blog with markdown support
- Contact form with email notifications
2. Task Management System:
- User authentication
- Real-time updates
- Team collaboration
- File uploads
- Email notifications
3. E-commerce Store:
- Product catalog
- Shopping cart
- Checkout flow
- Payment integration
- Order management
4. Social Media Dashboard:
- User profiles
- Posts and comments
- Image uploads
- Like/follow system
- Real-time notifications
5. Learning Platform:
- Course catalog
- Video lessons
- Progress tracking
- Quizzes and assessments
- Certificates
Further Resources
Production Best Practices:
Security:
Performance:
DevOps:
Monitoring:
Congratulations!
You've completed the BIUH Web Development Course!
You started from web fundamentals and progressed through:
- HTML structure and semantics
- CSS styling and layouts
- JavaScript interactivity
- Vue.js framework
- Backend development with FastAPI
- Production deployment
You're now equipped to build, deploy, and maintain modern full-stack web applications. Keep building, keep learning, and most importantly, keep creating!
The journey doesn't end here - it's just beginning.
Happy coding!