Best Practices

Follow deployment best practices for secure, reliable, and maintainable applications.

Best Practices

Security Best Practices

Environment Security

Protect sensitive configuration and credentials.

// lib/security-config.ts
import { z } from 'zod'

// Environment variable validation schema
const securityConfigSchema = z.object({
  // Required security settings
  NEXTAUTH_SECRET: z.string().min(32, 'Secret must be at least 32 characters'),
  DATABASE_URL: z.string().refine(
    (url) => !url.includes('localhost') || process.env.NODE_ENV !== 'production',
    'Production database cannot be localhost'
  ),
  
  // Optional security headers
  SECURITY_HEADERS_ENABLED: z.string().optional().default('true'),
  CSP_ENABLED: z.string().optional().default('true'),
})

export function validateSecurityConfig() {
  try {
    return securityConfigSchema.parse(process.env)
  } catch (error) {
    console.error('❌ Security configuration validation failed')
    throw error
  }
}

// Security headers middleware
export function securityHeaders() {
  return {
    'X-Frame-Options': 'DENY',
    'X-Content-Type-Options': 'nosniff',
    'Referrer-Policy': 'strict-origin-when-cross-origin',
    'Permissions-Policy': 'camera=(), microphone=(), geolocation=()',
    'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
    'Content-Security-Policy': [
      "default-src 'self'",
      "script-src 'self' 'unsafe-eval' 'unsafe-inline'",
      "style-src 'self' 'unsafe-inline'",
      "img-src 'self' data: https:",
      "font-src 'self'",
      "connect-src 'self'",
    ].join('; '),
  }
}

Access Control

Implement proper authentication and authorization.

// lib/auth-middleware.ts
import { NextRequest, NextResponse } from 'next/server'
import jwt from 'jsonwebtoken'

export interface AuthContext {
  userId: string
  role: string
  permissions: string[]
}

export function authMiddleware(requiredPermissions: string[] = []) {
  return async (req: NextRequest) => {
    const authHeader = req.headers.get('authorization')
    
    if (!authHeader?.startsWith('Bearer ')) {
      return NextResponse.json(
        { error: 'Authentication required' },
        { status: 401 }
      )
    }
    
    try {
      const token = authHeader.substring(7)
      const payload = jwt.verify(token, process.env.JWT_SECRET!) as any
      
      // Check permissions
      if (requiredPermissions.length > 0) {
        const hasPermission = requiredPermissions.every(permission =>
          payload.permissions.includes(permission)
        )
        
        if (!hasPermission) {
          return NextResponse.json(
            { error: 'Insufficient permissions' },
            { status: 403 }
          )
        }
      }
      
      // Add auth context to request
      const authContext: AuthContext = {
        userId: payload.userId,
        role: payload.role,
        permissions: payload.permissions,
      }
      
      // Continue to next middleware/handler
      const response = NextResponse.next()
      response.headers.set('x-auth-context', JSON.stringify(authContext))
      return response
      
    } catch (error) {
      return NextResponse.json(
        { error: 'Invalid token' },
        { status: 401 }
      )
    }
  }
}

Performance Best Practices

Code Optimization

Write efficient, performant code.

// lib/optimization-utils.ts

// Memoization for expensive calculations
export function memoize<T extends (...args: any[]) => any>(fn: T): T {
  const cache = new Map()
  
  return ((...args: any[]) => {
    const key = JSON.stringify(args)
    
    if (cache.has(key)) {
      return cache.get(key)
    }
    
    const result = fn(...args)
    cache.set(key, result)
    return result
  }) as T
}

// Debounce for rate limiting
export function debounce<T extends (...args: any[]) => any>(
  fn: T,
  delay: number
): T {
  let timeoutId: NodeJS.Timeout
  
  return ((...args: any[]) => {
    clearTimeout(timeoutId)
    timeoutId = setTimeout(() => fn(...args), delay)
  }) as T
}

// Throttle for performance-sensitive operations
export function throttle<T extends (...args: any[]) => any>(
  fn: T,
  limit: number
): T {
  let inThrottle: boolean
  
  return ((...args: any[]) => {
    if (!inThrottle) {
      fn(...args)
      inThrottle = true
      setTimeout(() => (inThrottle = false), limit)
    }
  }) as T
}

// Lazy loading utility
export function lazyLoad<T>(loader: () => Promise<T>): () => Promise<T> {
  let cached: T | null = null
  let loading: Promise<T> | null = null
  
  return async () => {
    if (cached) return cached
    if (loading) return loading
    
    loading = loader()
    cached = await loading
    loading = null
    
    return cached
  }
}

Database Optimization

Optimize database queries and connections.

// lib/db-optimization.ts
import { Pool, PoolClient } from 'pg'

export class OptimizedDatabase {
  private pool: Pool
  private queryCache = new Map<string, any>()
  
  constructor() {
    this.pool = new Pool({
      // Connection optimization
      max: 20,
      min: 5,
      idleTimeoutMillis: 30000,
      connectionTimeoutMillis: 2000,
      
      // Query optimization
      statement_timeout: 30000,
      query_timeout: 30000,
      
      // Connection pooling
      application_name: 'your-app',
    })
  }
  
  // Prepared statement caching
  async query(sql: string, params: any[] = []): Promise<any> {
    const cacheKey = `${sql}:${JSON.stringify(params)}`
    
    // Check cache for read queries
    if (sql.trim().toLowerCase().startsWith('select') && this.queryCache.has(cacheKey)) {
      const cached = this.queryCache.get(cacheKey)
      if (Date.now() - cached.timestamp < 60000) { // 1 minute cache
        return cached.result
      }
    }
    
    const client = await this.pool.connect()
    try {
      const result = await client.query(sql, params)
      
      // Cache read queries
      if (sql.trim().toLowerCase().startsWith('select')) {
        this.queryCache.set(cacheKey, {
          result,
          timestamp: Date.now(),
        })
      }
      
      return result
    } finally {
      client.release()
    }
  }
  
  // Bulk operations
  async bulkInsert(table: string, data: any[]): Promise<void> {
    if (data.length === 0) return
    
    const columns = Object.keys(data[0])
    const values = data.map(row => columns.map(col => row[col]))
    
    const placeholders = data.map((_, i) => 
      `(${columns.map((_, j) => `$${i * columns.length + j + 1}`).join(', ')})`
    ).join(', ')
    
    const sql = `INSERT INTO ${table} (${columns.join(', ')}) VALUES ${placeholders}`
    const params = values.flat()
    
    await this.query(sql, params)
  }
  
  // Transaction helper
  async transaction<T>(callback: (client: PoolClient) => Promise<T>): Promise<T> {
    const client = await this.pool.connect()
    
    try {
      await client.query('BEGIN')
      const result = await callback(client)
      await client.query('COMMIT')
      return result
    } catch (error) {
      await client.query('ROLLBACK')
      throw error
    } finally {
      client.release()
    }
  }
}

Reliability Best Practices

Error Handling

Implement comprehensive error handling.

// lib/error-handling.ts
export class ApplicationError extends Error {
  constructor(
    message: string,
    public statusCode: number = 500,
    public code?: string
  ) {
    super(message)
    this.name = 'ApplicationError'
  }
}

export class ValidationError extends ApplicationError {
  constructor(message: string, public field?: string) {
    super(message, 400, 'VALIDATION_ERROR')
    this.name = 'ValidationError'
  }
}

export class DatabaseError extends ApplicationError {
  constructor(message: string, public originalError?: Error) {
    super(message, 500, 'DATABASE_ERROR')
    this.name = 'DatabaseError'
  }
}

// Global error handler
export function globalErrorHandler(error: Error): {
  message: string
  statusCode: number
  code?: string
} {
  // Log error for monitoring
  console.error('Application Error:', {
    name: error.name,
    message: error.message,
    stack: error.stack,
    timestamp: new Date().toISOString(),
  })
  
  // Handle specific error types
  if (error instanceof ApplicationError) {
    return {
      message: error.message,
      statusCode: error.statusCode,
      code: error.code,
    }
  }
  
  // Handle unknown errors
  return {
    message: 'Internal server error',
    statusCode: 500,
    code: 'INTERNAL_ERROR',
  }
}

// Async error wrapper
export function asyncHandler(fn: Function) {
  return (req: any, res: any, next: any) => {
    Promise.resolve(fn(req, res, next)).catch(next)
  }
}

Graceful Shutdown

Handle application shutdown gracefully.

// lib/graceful-shutdown.ts
export class GracefulShutdown {
  private shutdownHandlers: (() => Promise<void>)[] = []
  private isShuttingDown = false
  
  constructor() {
    process.on('SIGTERM', this.handleShutdown.bind(this))
    process.on('SIGINT', this.handleShutdown.bind(this))
  }
  
  addShutdownHandler(handler: () => Promise<void>): void {
    this.shutdownHandlers.push(handler)
  }
  
  private async handleShutdown(signal: string): Promise<void> {
    if (this.isShuttingDown) return
    
    console.log(`Received ${signal}. Starting graceful shutdown...`)
    this.isShuttingDown = true
    
    try {
      // Stop accepting new requests
      await this.stopServer()
      
      // Execute shutdown handlers
      await Promise.all(
        this.shutdownHandlers.map(handler => 
          handler().catch(error => 
            console.error('Shutdown handler error:', error)
          )
        )
      )
      
      console.log('Graceful shutdown completed')
      process.exit(0)
    } catch (error) {
      console.error('Graceful shutdown failed:', error)
      process.exit(1)
    }
  }
  
  private async stopServer(): Promise<void> {
    // Implementation depends on your server setup
    // For Next.js, this might involve closing the HTTP server
  }
}

// Usage
const gracefulShutdown = new GracefulShutdown()

// Add cleanup handlers
gracefulShutdown.addShutdownHandler(async () => {
  await db.end() // Close database connections
})

gracefulShutdown.addShutdownHandler(async () => {
  // Clean up any other resources
})

Monitoring Best Practices

Comprehensive Logging

Implement structured, searchable logging.

// lib/logging.ts
import winston from 'winston'

export const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }),
    winston.format.json()
  ),
  defaultMeta: {
    service: 'your-app',
    environment: process.env.NODE_ENV,
  },
  transports: [
    new winston.transports.File({
      filename: 'logs/error.log',
      level: 'error',
    }),
    new winston.transports.File({
      filename: 'logs/combined.log',
    }),
  ],
})

// Console logging in development
if (process.env.NODE_ENV !== 'production') {
  logger.add(new winston.transports.Console({
    format: winston.format.simple()
  }))
}

// Structured logging helpers
export const log = {
  info: (message: string, meta?: any) => logger.info(message, meta),
  warn: (message: string, meta?: any) => logger.warn(message, meta),
  error: (message: string, error?: Error, meta?: any) => {
    logger.error(message, { error: error?.message, stack: error?.stack, ...meta })
  },
  
  // Context-specific loggers
  request: (req: any, res: any, duration: number) => {
    logger.info('HTTP Request', {
      method: req.method,
      url: req.url,
      statusCode: res.statusCode,
      duration,
      userAgent: req.headers['user-agent'],
      ip: req.ip,
    })
  },
  
  database: (query: string, duration: number, error?: Error) => {
    if (error) {
      logger.error('Database Query Failed', {
        query: query.substring(0, 100), // Truncate long queries
        duration,
        error: error.message,
      })
    } else {
      logger.info('Database Query', {
        query: query.substring(0, 100),
        duration,
      })
    }
  },
}

Metrics Collection

Track key application metrics.

// lib/metrics.ts
export class MetricsCollector {
  private counters = new Map<string, number>()
  private gauges = new Map<string, number>()
  private histograms = new Map<string, number[]>()
  
  // Counter metrics (cumulative)
  increment(name: string, value = 1, tags?: Record<string, string>): void {
    const key = this.createKey(name, tags)
    this.counters.set(key, (this.counters.get(key) || 0) + value)
  }
  
  // Gauge metrics (current value)
  gauge(name: string, value: number, tags?: Record<string, string>): void {
    const key = this.createKey(name, tags)
    this.gauges.set(key, value)
  }
  
  // Histogram metrics (distribution)
  histogram(name: string, value: number, tags?: Record<string, string>): void {
    const key = this.createKey(name, tags)
    if (!this.histograms.has(key)) {
      this.histograms.set(key, [])
    }
    this.histograms.get(key)!.push(value)
  }
  
  // Timing helper
  time<T>(name: string, fn: () => Promise<T>, tags?: Record<string, string>): Promise<T> {
    const startTime = Date.now()
    return fn().finally(() => {
      this.histogram(name, Date.now() - startTime, tags)
    })
  }
  
  private createKey(name: string, tags?: Record<string, string>): string {
    if (!tags) return name
    const tagString = Object.entries(tags)
      .map(([k, v]) => `${k}:${v}`)
      .join(',')
    return `${name}{${tagString}}`
  }
  
  // Export metrics for monitoring systems
  export(): any {
    return {
      counters: Object.fromEntries(this.counters),
      gauges: Object.fromEntries(this.gauges),
      histograms: Object.fromEntries(
        Array.from(this.histograms.entries()).map(([key, values]) => [
          key,
          {
            count: values.length,
            sum: values.reduce((a, b) => a + b, 0),
            avg: values.length > 0 ? values.reduce((a, b) => a + b, 0) / values.length : 0,
            min: Math.min(...values),
            max: Math.max(...values),
          },
        ])
      ),
    }
  }
}

export const metrics = new MetricsCollector()

// Decorator for automatic method timing
export function timed(metricName: string) {
  return function(target: any, propertyName: string, descriptor: PropertyDescriptor) {
    const method = descriptor.value
    descriptor.value = async function(...args: any[]) {
      return metrics.time(metricName, () => method.apply(this, args))
    }
  }
}

Follow these best practices to build secure, reliable, and maintainable applications that scale effectively.