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.