1. Latar Belakang
Memasuki minggu keempat, hari kedua kita masih akan belajar Express.js di Perwira Learning Center dan akan membahas Middleware Lanjutan dan Alur Eksekusi. Jika sebelumnya kita belajar middleware dasar, kini kita akan menyelami lebih dalam tentang bagaimana middleware bekerja di balik layar, bagaimana mengontrol alur eksekusi, dan bagaimana membangun custom middleware yang powerful.
Analogi Middleware Lanjutan: Bandara Internasional
Bayangkan aplikasi Express.js adalah sebuah bandara:
- Request = Penumpang yang datang
- Response = Penumpang yang keluar
- Middleware = Pos-pos pemeriksaan
- Security Check = Autentikasi & Authorisasi
- Imigrasi = Validasi data
- X-Ray Scanner = Logging & Monitoring
- Boarding Gate = Rate Limiting
- Customs = Transformasi data
Setiap middleware bisa:
- Menghentikan penumpang jika ada masalah (res.json)
- Memproses dan melanjutkan ke pos berikutnya (next())
- Melewati beberapa pos tertentu (next('route'))
- Mengirim ke pos khusus jika ada masalah (next(error))
2. Alat dan Bahan
a. Perangkat Lunak
- Node.js & npm - Runtime dan package manager
- Express.js - Framework utama
- Postman - Testing middleware behavior
- VS Code - Code editor
- Git - Version control
b. Perangkat Keras
- Laptop/PC dengan spesifikasi standar
3. Pembahasan
3.1 Anatomi Middleware Lanjutan
/** * ANATOMI MIDDLEWARE LENGKAP * Setiap middleware memiliki akses ke 3/4 parameter: * - req : Request object * - res : Response object * - next : Function untuk melanjutkan ke middleware berikutnya * - err : Error object (khusus error handler) */ // 1. Standard Middleware (3 parameter) function standardMiddleware(req, res, next) { // Proses request console.log('Processing...'); next(); // Lanjut ke middleware berikutnya } // 2. Error Handling Middleware (4 parameter) function errorHandler(err, req, res, next) { // Tangani error console.error(err.stack); res.status(500).send('Something broke!'); } // 3. Middleware dengan Configuration function createMiddleware(options) { return function(req, res, next) { // Gunakan options di sini if (options.logging) { console.log(req.method, req.url); } next(); }; } // 4. Async Middleware async function asyncMiddleware(req, res, next) { try { const data = await fetchData(); req.data = data; next(); } catch (error) { next(error); // Pass error ke error handler } }
3.2 Praktik Lengkap: Sistem Middleware Enterprise
Mari kita bangun sistem middleware yang kompleks untuk aplikasi enterprise:
# 1. Buat folder project mkdir enterprise-middleware cd enterprise-middleware # 2. Inisialisasi project npm init -y # 3. Install dependencies npm install express dotenv cors helmet morgan compression npm install express-rate-limit express-slow-down npm install jsonwebtoken bcryptjs joi npm install winston winston-daily-rotate-file npm install -D nodemon # 4. Buat struktur folder mkdir -p src/{middleware,config,utils,services,models,routes}
src/utils/logger.js
/** * LOGGER MIDDLEWARE - Winston Advanced Logger * Fitur: * - Multiple transports (console, file, daily rotate) * - Different log levels per environment * - Request/Response logging * - Error tracking * - Performance monitoring */ const winston = require('winston'); const DailyRotateFile = require('winston-daily-rotate-file'); // Define custom log levels const levels = { error: 0, warn: 1, info: 2, http: 3, debug: 4, audit: 5 }; // Define colors const colors = { error: 'red', warn: 'yellow', info: 'green', http: 'magenta', debug: 'blue', audit: 'cyan' }; winston.addColors(colors); // Custom format untuk audit trail const auditFormat = winston.format.printf(({ timestamp, level, message, ...meta }) => { return JSON.stringify({ timestamp, level, ...meta, message }); }); // Create logger instance const logger = winston.createLogger({ levels, level: process.env.LOG_LEVEL || 'info', format: winston.format.combine( winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), winston.format.errors({ stack: true }), winston.format.splat(), winston.format.json() ), transports: [ // Console transport untuk development new winston.transports.Console({ format: winston.format.combine( winston.format.colorize({ all: true }), winston.format.printf( (info) => `${info.timestamp} ${info.level}: ${info.message}` ) ) }), // File transport untuk semua logs new DailyRotateFile({ filename: 'logs/application-%DATE%.log', datePattern: 'YYYY-MM-DD', maxSize: '20m', maxFiles: '14d', format: winston.format.combine( winston.format.uncolorize(), winston.format.json() ) }), // Separate file untuk audit trail new DailyRotateFile({ filename: 'logs/audit-%DATE%.log', datePattern: 'YYYY-MM-DD', maxSize: '20m', maxFiles: '30d', level: 'audit', format: winston.format.combine( winston.format.uncolorize(), auditFormat ) }), // Separate file untuk errors new DailyRotateFile({ filename: 'logs/error-%DATE%.log', datePattern: 'YYYY-MM-DD', maxSize: '20m', maxFiles: '30d', level: 'error' }) ] }); // Middleware untuk logging HTTP requests logger.httpLogger = function() { return function(req, res, next) { const start = process.hrtime(); // Generate unique request ID req.requestId = req.headers['x-request-id'] || `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; // Log request logger.http({ message: `${req.method} ${req.originalUrl}`, requestId: req.requestId, method: req.method, url: req.originalUrl, ip: req.ip || req.connection.remoteAddress, userAgent: req.headers['user-agent'], userId: req.user?.id, timestamp: new Date().toISOString() }); // Override res.end untuk log response const originalEnd = res.end; res.end = function(chunk, encoding) { const duration = process.hrtime(start); const durationMs = (duration[0] * 1e3 + duration[1] * 1e-6).toFixed(2); // Log response logger.http({ message: `${req.method} ${req.originalUrl} ${res.statusCode}`, requestId: req.requestId, statusCode: res.statusCode, duration: `${durationMs}ms`, contentLength: res.getHeader('content-length') || 0 }); originalEnd.call(this, chunk, encoding); }; next(); }; }; // Middleware untuk audit trail logger.audit = function(action, userId, details = {}) { logger.log('audit', { message: `Audit: ${action}`, action, userId, ...details, timestamp: new Date().toISOString() }); }; module.exports = logger;
B. Security Middleware Suite
src/middleware/security.middleware.js
/** * SECURITY MIDDLEWARE SUITE * Komprehensif: Helmet, CORS, CSP, HSTS, XSS Protection */ const helmet = require('helmet'); const cors = require('cors'); const config = require('../config/env'); const logger = require('../utils/logger'); class SecurityMiddleware { /** * Advanced Helmet Configuration */ static helmetConfig() { return helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'"], scriptSrc: ["'self'"], imgSrc: ["'self'", "data:", "https:"], connectSrc: ["'self'", config.API_URL], fontSrc: ["'self'", "https:"], objectSrc: ["'none'"], mediaSrc: ["'self'"], frameSrc: ["'none'"], }, }, hsts: { maxAge: 31536000, includeSubDomains: true, preload: true }, referrerPolicy: { policy: 'strict-origin-when-cross-origin' }, noSniff: true, xssFilter: true, hidePoweredBy: true, frameguard: { action: 'deny' } }); } /** * Dynamic CORS Configuration */ static corsConfig() { const whitelist = config.CORS_WHITELIST || [ 'http://localhost:3000', 'http://localhost:5173', 'https://yourdomain.com' ]; return cors({ origin: function(origin, callback) { // Allow requests with no origin (like mobile apps, curl, Postman) if (!origin || whitelist.indexOf(origin) !== -1) { callback(null, true); } else { logger.warn(`CORS blocked: ${origin}`); callback(new Error('Not allowed by CORS')); } }, credentials: true, methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], allowedHeaders: [ 'Content-Type', 'Authorization', 'X-Request-ID', 'X-API-Key', 'X-CSRF-Token' ], exposedHeaders: [ 'X-RateLimit-Limit', 'X-RateLimit-Remaining', 'X-RateLimit-Reset', 'X-Request-ID' ], maxAge: 86400 // 24 hours }); } /** * CSRF Protection (untuk form submissions) */ static csrfProtection() { return (req, res, next) => { // Untuk API, biasanya pakai token di header const csrfToken = req.headers['x-csrf-token']; const sessionToken = req.session?.csrfToken; if (req.method !== 'GET' && req.method !== 'HEAD' && req.method !== 'OPTIONS') { if (!csrfToken || !sessionToken || csrfToken !== sessionToken) { logger.warn('CSRF attack detected', { ip: req.ip, method: req.method, url: req.url }); return res.status(403).json({ success: false, error: 'CSRF token invalid' }); } } next(); }; } /** * API Key Authentication */ static apiKeyAuth() { return (req, res, next) => { const apiKey = req.headers['x-api-key']; const validApiKeys = config.API_KEYS || ['dev-key-123', 'prod-key-456']; if (!apiKey || !validApiKeys.includes(apiKey)) { logger.warn(`Invalid API key attempt: ${apiKey}`); return res.status(401).json({ success: false, error: 'Invalid API key' }); } req.apiKey = apiKey; next(); }; } /** * Sanitization Middleware (XSS Prevention) */ static sanitize() { return (req, res, next) => { // Sanitize request body if (req.body) { Object.keys(req.body).forEach(key => { if (typeof req.body[key] === 'string') { // Basic XSS sanitization req.body[key] = req.body[key] .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, ''') .replace(/\//g, '/'); } }); } // Sanitize query parameters if (req.query) { Object.keys(req.query).forEach(key => { if (typeof req.query[key] === 'string') { req.query[key] = req.query[key] .replace(/</g, '<') .replace(/>/g, '>'); } }); } next(); }; } /** * SQL Injection Prevention (Basic) */ static preventSQLInjection() { const sqlPattern = /(\b(SELECT|INSERT|UPDATE|DELETE|DROP|UNION|ALTER|CREATE|WHERE|FROM|JOIN)\b)|('--)|(;\s*$)/i; return (req, res, next) => { const checkValue = (value) => { if (typeof value === 'string' && sqlPattern.test(value)) { return true; } return false; }; // Check query parameters for (const key in req.query) { if (checkValue(req.query[key])) { logger.warn(`SQL injection attempt detected in query: ${key}=${req.query[key]}`); return res.status(400).json({ success: false, error: 'Invalid input detected' }); } } // Check body if (req.body) { for (const key in req.body) { if (checkValue(req.body[key])) { logger.warn(`SQL injection attempt detected in body: ${key}=${req.body[key]}`); return res.status(400).json({ success: false, error: 'Invalid input detected' }); } } } next(); }; } } module.exports = SecurityMiddleware;
C. Advanced Rate Limiting
src/middleware/rate-limit.middleware.js
/** * ADVANCED RATE LIMITING MIDDLEWARE * Fitur: * - Per-user & per-IP rate limiting * - Different limits per endpoint * - Sliding window algorithm * - Graceful degradation * - Redis support for distributed systems */ const rateLimit = require('express-rate-limit'); const slowDown = require('express-slow-down'); const RedisStore = require('rate-limit-redis'); const Redis = require('ioredis'); const config = require('../config/env'); const logger = require('../utils/logger'); class RateLimitMiddleware { constructor() { this.redis = config.REDIS_URL ? new Redis(config.REDIS_URL) : null; } /** * Standard rate limiter untuk semua endpoint */ static standard() { return rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // Limit each IP to 100 requests per windowMs message: { success: false, error: 'Too many requests', message: 'Please try again later' }, standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers legacyHeaders: false, // Disable the `X-RateLimit-*` headers keyGenerator: (req) => { // Use user ID if authenticated, otherwise IP return req.user?.id || req.ip; }, handler: (req, res) => { logger.warn('Rate limit exceeded', { ip: req.ip, userId: req.user?.id, path: req.path }); res.status(429).json({ success: false, error: 'RATE_LIMIT_EXCEEDED', message: 'Too many requests, please try again later', retryAfter: Math.ceil(req.rateLimit.resetTime / 1000) }); }, skip: (req) => { // Skip rate limiting for certain conditions return req.path === '/health' || req.user?.role === 'admin'; } }); } /** * Strict rate limiter untuk auth endpoints */ static strict() { return rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 5, // Only 5 requests per 15 minutes skipSuccessfulRequests: true, // Don't count successful requests message: { success: false, error: 'Too many authentication attempts', message: 'Please try again after 15 minutes' }, keyGenerator: (req) => { // Auth attempts are tracked by email or IP return req.body.email || req.ip; } }); } /** * Slow down middleware (gradual throttling) */ static slowDown() { return slowDown({ windowMs: 15 * 60 * 1000, // 15 minutes delayAfter: 50, // Allow 50 requests without delay delayMs: (hits) => hits * 100, // Add 100ms delay per hit above limit maxDelayMs: 10000, // Max 10 second delay skip: (req) => req.user?.role === 'admin' }); } /** * Per-endpoint custom rate limiter factory */ static endpointLimiter(options = {}) { const { windowMs = 60 * 1000, max = 30, message = 'Too many requests to this endpoint', keyGenerator = (req) => req.user?.id || req.ip } = options; return rateLimit({ windowMs, max, message: { success: false, error: 'ENDPOINT_RATE_LIMIT', message }, keyGenerator, standardHeaders: true, legacyHeaders: false }); } /** * Concurrent request limiter */ static concurrent(limit = 10) { const activeRequests = new Map(); return (req, res, next) => { const key = req.user?.id || req.ip; const current = activeRequests.get(key) || 0; if (current >= limit) { logger.warn(`Concurrent request limit exceeded for ${key}`); return res.status(429).json({ success: false, error: 'CONCURRENT_LIMIT_EXCEEDED', message: 'Too many concurrent requests', limit }); } activeRequests.set(key, current + 1); const complete = () => { const remaining = activeRequests.get(key) || 1; if (remaining <= 1) { activeRequests.delete(key); } else { activeRequests.set(key, remaining - 1); } }; res.on('finish', complete); res.on('error', complete); next(); }; } /** * Bandwidth limiter */ static bandwidth(limitBytes = 1024 * 1024) { // 1MB default const usage = new Map(); return (req, res, next) => { const key = req.user?.id || req.ip; const used = usage.get(key) || 0; if (used >= limitBytes) { logger.warn(`Bandwidth limit exceeded for ${key}`); return res.status(429).json({ success: false, error: 'BANDWIDTH_LIMIT_EXCEEDED', message: 'Bandwidth limit exceeded' }); } // Track response size const originalWrite = res.write; const originalEnd = res.end; let bytesWritten = 0; res.write = function(chunk, encoding) { bytesWritten += Buffer.byteLength(chunk, encoding); return originalWrite.call(this, chunk, encoding); }; res.end = function(chunk, encoding) { if (chunk) { bytesWritten += Buffer.byteLength(chunk, encoding); } const totalUsed = used + bytesWritten; usage.set(key, totalUsed); // Add bandwidth headers res.setHeader('X-Bandwidth-Used', bytesWritten); res.setHeader('X-Bandwidth-Limit', limitBytes); res.setHeader('X-Bandwidth-Remaining', limitBytes - totalUsed); return originalEnd.call(this, chunk, encoding); }; next(); }; } } module.exports = RateLimitMiddleware;
D. Authentication & Authorization Suite
src/middleware/auth.middleware.js
/** * ADVANCED AUTHENTICATION MIDDLEWARE * Fitur: * - JWT token validation * - Role-based access control (RBAC) * - Permission-based access control * - Token refresh mechanism * - Device fingerprinting * - Session management */ const jwt = require('jsonwebtoken'); const bcrypt = require('bcryptjs'); const crypto = require('crypto'); const config = require('../config/env'); const logger = require('../utils/logger'); const Redis = require('ioredis'); class AuthMiddleware { constructor() { this.redis = config.REDIS_URL ? new Redis(config.REDIS_URL) : null; this.secret = config.JWT_SECRET; this.refreshSecret = config.JWT_REFRESH_SECRET || this.secret; } /** * Generate device fingerprint */ static generateFingerprint(req) { const components = [ req.headers['user-agent'] || '', req.headers['accept-language'] || '', req.headers['sec-ch-ua'] || '', req.ip || '', req.headers['accept-encoding'] || '' ].join('|'); return crypto .createHash('sha256') .update(components) .digest('hex'); } /** * Main authentication middleware */ authenticate() { return async (req, res, next) => { try { // Extract token from various locations const token = this.extractToken(req); if (!token) { throw { name: 'AuthenticationError', message: 'No token provided', code: 'NO_TOKEN', statusCode: 401 }; } // Verify token const decoded = await this.verifyToken(token, this.secret); // Check if token is blacklisted if (this.redis) { const isBlacklisted = await this.redis.get(`blacklist:${token}`); if (isBlacklisted) { throw { name: 'AuthenticationError', message: 'Token has been revoked', code: 'TOKEN_REVOKED', statusCode: 401 }; } } // Verify device fingerprint const fingerprint = AuthMiddleware.generateFingerprint(req); if (decoded.fingerprint && decoded.fingerprint !== fingerprint) { logger.warn(`Device fingerprint mismatch`, { userId: decoded.sub, expected: decoded.fingerprint, received: fingerprint }); throw { name: 'AuthenticationError', message: 'Invalid device fingerprint', code: 'INVALID_DEVICE', statusCode: 401 }; } // Attach user info to request req.user = { id: decoded.sub, email: decoded.email, role: decoded.role, permissions: decoded.permissions || [] }; req.token = token; req.tokenDecoded = decoded; // Log successful authentication logger.info('User authenticated', { userId: req.user.id, role: req.user.role, path: req.path, requestId: req.requestId }); next(); } catch (error) { next(error); } }; } /** * Refresh token authentication */ authenticateRefresh() { return async (req, res, next) => { try { const refreshToken = req.body.refreshToken || req.headers['x-refresh-token']; if (!refreshToken) { throw { name: 'AuthenticationError', message: 'No refresh token provided', code: 'NO_REFRESH_TOKEN', statusCode: 401 }; } const decoded = await this.verifyToken(refreshToken, this.refreshSecret); // Verify in Redis if (this.redis) { const stored = await this.redis.get(`refresh:${decoded.sub}`); if (stored !== refreshToken) { throw { name: 'AuthenticationError', message: 'Invalid refresh token', code: 'INVALID_REFRESH_TOKEN', statusCode: 401 }; } } req.refreshToken = refreshToken; req.user = { id: decoded.sub }; next(); } catch (error) { next(error); } }; } /** * Role-based authorization */ authorize(roles = []) { return (req, res, next) => { try { if (!req.user) { throw { name: 'AuthorizationError', message: 'User not authenticated', code: 'NOT_AUTHENTICATED', statusCode: 401 }; } if (roles.length && !roles.includes(req.user.role)) { logger.warn('Authorization failed', { userId: req.user.id, requiredRole: roles, userRole: req.user.role, path: req.path }); throw { name: 'AuthorizationError', message: 'Insufficient permissions', code: 'FORBIDDEN', statusCode: 403 }; } next(); } catch (error) { next(error); } }; } /** * Permission-based authorization (fine-grained) */ hasPermission(permissions = []) { return (req, res, next) => { try { if (!req.user) { throw { name: 'AuthorizationError', message: 'User not authenticated', code: 'NOT_AUTHENTICATED', statusCode: 401 }; } const userPermissions = req.user.permissions || []; const hasAllPermissions = permissions.every(p => userPermissions.includes(p) || req.user.role === 'admin' ); if (!hasAllPermissions) { logger.warn('Permission denied', { userId: req.user.id, requiredPermissions: permissions, userPermissions }); throw { name: 'AuthorizationError', message: 'Missing required permissions', code: 'PERMISSION_DENIED', statusCode: 403, details: { required: permissions } }; } next(); } catch (error) { next(error); } }; } /** * Resource ownership verification */ verifyOwnership(resourceGetter) { return async (req, res, next) => { try { const resource = await resourceGetter(req); if (!resource) { throw { name: 'NotFoundError', message: 'Resource not found', code: 'NOT_FOUND', statusCode: 404 }; } // Admin can access any resource if (req.user.role === 'admin') { return next(); } // Check ownership if (resource.userId !== req.user.id && resource.authorId !== req.user.id) { logger.warn('Ownership verification failed', { userId: req.user.id, resourceId: req.params.id, resourceOwner: resource.userId || resource.authorId }); throw { name: 'AuthorizationError', message: 'You do not own this resource', code: 'NOT_OWNER', statusCode: 403 }; } next(); } catch (error) { next(error); } }; } /** * Extract token from request */ extractToken(req) { // Check Authorization header const authHeader = req.headers.authorization; if (authHeader && authHeader.startsWith('Bearer ')) { return authHeader.split(' ')[1]; } // Check query parameter if (req.query.token) { return req.query.token; } // Check cookie if (req.cookies && req.cookies.token) { return req.cookies.token; } return null; } /** * Verify JWT token */ async verifyToken(token, secret) { try { const decoded = jwt.verify(token, secret); return decoded; } catch (error) { if (error.name === 'TokenExpiredError') { throw { name: 'AuthenticationError', message: 'Token expired', code: 'TOKEN_EXPIRED', statusCode: 401 }; } throw { name: 'AuthenticationError', message: 'Invalid token', code: 'INVALID_TOKEN', statusCode: 401 }; } } /** * Optional authentication (doesn't throw error) */ optional() { return async (req, res, next) => { try { const token = this.extractToken(req); if (token) { const decoded = await this.verifyToken(token, this.secret); req.user = { id: decoded.sub, email: decoded.email, role: decoded.role }; } next(); } catch { // Silently fail, proceed without user next(); } }; } } module.exports = new AuthMiddleware();
E. Validation Middleware Factory
src/middleware/validation.middleware.js
/** * ADVANCED VALIDATION MIDDLEWARE * Fitur: * - Schema validation with Joi * - Conditional validation * - Async validation * - Custom validators * - Validation error formatting */ const Joi = require('joi'); const logger = require('../utils/logger'); class ValidationMiddleware { /** * Main validation middleware factory */ static validate(schema, property = 'body', options = {}) { return async (req, res, next) => { try { const { abortEarly = false, stripUnknown = true, allowUnknown = false, convert = true, context = {} } = options; // Add request context to schema const validationContext = { ...context, user: req.user, params: req.params, query: req.query }; // Apply conditional validation const finalSchema = typeof schema === 'function' ? schema(validationContext) : schema; const value = await finalSchema.validateAsync(req[property], { abortEarly, stripUnknown, allowUnknown, convert, context: validationContext }); // Replace request data with validated data req[property] = value; // Log validation success logger.debug('Validation passed', { property, path: req.path }); next(); } catch (error) { if (error.isJoi) { const errors = error.details.map(detail => ({ field: detail.path.join('.'), message: detail.message.replace(/"/g, ''), type: detail.type, context: detail.context })); logger.warn('Validation failed', { property, errors, path: req.path, body: req.body }); return res.status(400).json({ success: false, error: 'VALIDATION_ERROR', message: 'Validation failed', errors }); } next(error); } }; } /** * Dynamic schema builder */ static createSchema(rules) { const schema = {}; Object.entries(rules).forEach(([field, rule]) => { let validator = Joi[rule.type](); // Apply constraints if (rule.required) validator = validator.required(); if (rule.min) validator = validator.min(rule.min); if (rule.max) validator = validator.max(rule.max); if (rule.email) validator = validator.email(); if (rule.pattern) validator = validator.pattern(rule.pattern); if (rule.default) validator = validator.default(rule.default); if (rule.valid) validator = validator.valid(...rule.valid); if (rule.invalid) validator = validator.invalid(...rule.invalid); schema[field] = validator; }); return Joi.object(schema); } /** * Conditional validation middleware */ static conditional(condition, schema) { return async (req, res, next) => { const shouldValidate = typeof condition === 'function' ? condition(req) : condition; if (shouldValidate) { return this.validate(schema)(req, res, next); } next(); }; } /** * File validation middleware (for multer) */ static validateFile(options = {}) { const { maxSize = 5 * 1024 * 1024, // 5MB allowedTypes = ['image/jpeg', 'image/png', 'image/gif'], maxCount = 1 } = options; return (req, res, next) => { const files = req.files || (req.file ? [req.file] : []); if (files.length === 0) { return next(); } const errors = []; files.forEach((file, index) => { // Check file size if (file.size > maxSize) { errors.push({ field: file.fieldname, index, error: `File too large. Max size: ${maxSize / 1024 / 1024}MB`, current: `${(file.size / 1024 / 1024).toFixed(2)}MB` }); } // Check file type if (!allowedTypes.includes(file.mimetype)) { errors.push({ field: file.fieldname, index, error: 'Invalid file type', allowed: allowedTypes, received: file.mimetype }); } }); if (errors.length > 0) { return res.status(400).json({ success: false, error: 'FILE_VALIDATION_ERROR', message: 'File validation failed', errors }); } next(); }; } } module.exports = ValidationMiddleware;
F. Performance & Monitoring Middleware
src/middleware/performance.middleware.js
/** * PERFORMANCE & MONITORING MIDDLEWARE * Fitur: * - Response time tracking * - Memory usage monitoring * - CPU usage tracking * - Slow request detection * - Performance metrics */ const os = require('os'); const process = require('process'); const logger = require('../utils/logger'); class PerformanceMiddleware { constructor() { this.slowRequests = []; } /** * Response time tracker */ static responseTime() { return (req, res, next) => { const start = process.hrtime(); res.on('finish', () => { const diff = process.hrtime(start); const responseTime = diff[0] * 1e3 + diff[1] * 1e-6; // Add response time header res.setHeader('X-Response-Time', `${responseTime.toFixed(2)}ms`); // Log slow requests if (responseTime > 1000) { logger.warn('Slow request detected', { method: req.method, url: req.originalUrl, responseTime: `${responseTime.toFixed(2)}ms`, userId: req.user?.id }); } }); next(); }; } /** * Memory usage monitor */ static memoryUsage() { return (req, res, next) => { const usage = process.memoryUsage(); const memoryThreshold = 500 * 1024 * 1024; // 500MB if (usage.heapUsed > memoryThreshold) { logger.warn('High memory usage detected', { heapUsed: `${(usage.heapUsed / 1024 / 1024).toFixed(2)}MB`, heapTotal: `${(usage.heapTotal / 1024 / 1024).toFixed(2)}MB`, rss: `${(usage.rss / 1024 / 1024).toFixed(2)}MB` }); } // Add memory header res.setHeader('X-Memory-Usage', `${(usage.heapUsed / 1024 / 1024).toFixed(2)}MB`); next(); }; } /** * CPU usage monitor */ static cpuUsage() { let lastCheck = process.cpuUsage(); let lastTime = Date.now(); return (req, res, next) => { const currentTime = Date.now(); const timeDiff = currentTime - lastTime; if (timeDiff > 5000) { // Check every 5 seconds const currentUsage = process.cpuUsage(); const userDiff = currentUsage.user - lastCheck.user; const systemDiff = currentUsage.system - lastCheck.system; const cpuPercent = ((userDiff + systemDiff) / (timeDiff * 1000)) * 100; if (cpuPercent > 80) { logger.warn('High CPU usage detected', { cpuPercent: `${cpuPercent.toFixed(2)}%`, user: `${(userDiff / 1000000).toFixed(2)}s`, system: `${(systemDiff / 1000000).toFixed(2)}s` }); } lastCheck = currentUsage; lastTime = currentTime; } next(); }; } /** * Request throttling (adaptive rate limiting) */ static adaptiveThrottling() { const requestTimes = []; const windowMs = 60000; // 1 minute return (req, res, next) => { const now = Date.now(); // Clean old entries while (requestTimes.length && requestTimes[0] < now - windowMs) { requestTimes.shift(); } // Add current request requestTimes.push(now); // Calculate requests per second const rps = requestTimes.length / (windowMs / 1000); // Adaptive delay based on load if (rps > 50) { // High load - add delay const delay = Math.min((rps - 50) * 5, 1000); // Max 1 second delay setTimeout(() => { res.setHeader('X-Adaptive-Delay', `${delay}ms`); res.setHeader('X-Requests-Per-Second', rps.toFixed(2)); next(); }, delay); } else { res.setHeader('X-Requests-Per-Second', rps.toFixed(2)); next(); } }; } /** * Database query performance monitor */ static queryPerformance() { return (req, res, next) => { // Store original query method const originalQuery = req.db?.query; if (req.db) { req.db.query = async function(sql, params) { const start = process.hrtime(); try { const result = await originalQuery.call(this, sql, params); const diff = process.hrtime(start); const duration = diff[0] * 1e3 + diff[1] * 1e-6; // Log slow queries if (duration > 100) { logger.warn('Slow query detected', { duration: `${duration.toFixed(2)}ms`, sql: sql.substring(0, 200), params }); } return result; } catch (error) { const diff = process.hrtime(start); const duration = diff[0] * 1e3 + diff[1] * 1e-6; logger.error('Query failed', { duration: `${duration.toFixed(2)}ms`, sql: sql.substring(0, 200), error: error.message }); throw error; } }; } next(); }; } /** * Request queue monitor */ static requestQueue() { let activeRequests = 0; let maxConcurrent = 0; return (req, res, next) => { activeRequests++; maxConcurrent = Math.max(maxConcurrent, activeRequests); // Add metrics header res.setHeader('X-Active-Requests', activeRequests); res.setHeader('X-Max-Concurrent', maxConcurrent); const complete = () => { activeRequests--; }; res.on('finish', complete); res.on('error', complete); next(); }; } } module.exports = PerformanceMiddleware;
G. Data Transformation Middleware
src/middleware/transform.middleware.js
/** * DATA TRANSFORMATION MIDDLEWARE * Fitur: * - Request/Response transformation * - Pagination formatting * - Field filtering * - Data enrichment * - Format conversion */ class TransformMiddleware { /** * Transform request body */ static transformRequest(transformFn) { return (req, res, next) => { if (req.body) { req.body = transformFn(req.body, req); } next(); }; } /** * Transform response data */ static transformResponse(transformFn) { return (req, res, next) => { const originalJson = res.json; res.json = function(data) { const transformed = transformFn(data, req); return originalJson.call(this, transformed); }; next(); }; } /** * Format pagination */ static paginate() { return (req, res, next) => { // Parse pagination parameters req.pagination = { page: Math.max(parseInt(req.query.page) || 1, 1), limit: Math.min( Math.max(parseInt(req.query.limit) || 10, 1), 100 ) }; req.pagination.offset = (req.pagination.page - 1) * req.pagination.limit; // Store original json const originalJson = res.json; res.json = function(data) { if (data && data.rows !== undefined) { const { rows, count } = data; const paginatedResponse = { data: rows, pagination: { page: req.pagination.page, limit: req.pagination.limit, totalItems: count, totalPages: Math.ceil(count / req.pagination.limit), hasNext: req.pagination.page < Math.ceil(count / req.pagination.limit), hasPrev: req.pagination.page > 1 } }; return originalJson.call(this, paginatedResponse); } return originalJson.call(this, data); }; next(); }; } /** * Field filtering (sparse fields) */ static filterFields() { return (req, res, next) => { const fields = req.query.fields ? req.query.fields.split(',') : null; if (fields) { const originalJson = res.json; res.json = function(data) { if (Array.isArray(data)) { const filtered = data.map(item => { const filteredItem = {}; fields.forEach(field => { if (item[field] !== undefined) { filteredItem[field] = item[field]; } }); return filteredItem; }); return originalJson.call(this, filtered); } if (data && typeof data === 'object') { const filtered = {}; fields.forEach(field => { if (data[field] !== undefined) { filtered[field] = data[field]; } }); return originalJson.call(this, filtered); } return originalJson.call(this, data); }; } next(); }; } /** * Data enrichment (add computed fields) */ static enrich(enricher) { return (req, res, next) => { const originalJson = res.json; res.json = function(data) { if (data && data.data) { data.data = enricher(data.data, req); } else if (data && Array.isArray(data)) { data = enricher(data, req); } return originalJson.call(this, data); }; next(); }; } /** * Format dates consistently */ static formatDate(format = 'ISO') { return (req, res, next) => { const originalJson = res.json; res.json = function(data) { const formatDates = (obj) => { if (!obj || typeof obj !== 'object') return; Object.keys(obj).forEach(key => { if (obj[key] instanceof Date) { if (format === 'ISO') { obj[key] = obj[key].toISOString(); } else if (format === 'timestamp') { obj[key] = obj[key].getTime(); } else if (format === 'locale') { obj[key] = obj[key].toLocaleString(); } } else if (typeof obj[key] === 'object') { formatDates(obj[key]); } }); }; formatDates(data); return originalJson.call(this, data); }; next(); }; } /** * JSON:API formatter */ static jsonApi() { return (req, res, next) => { const originalJson = res.json; res.json = function(data) { let response = {}; if (data && data.data !== undefined) { // Already formatted response = data; } else if (Array.isArray(data)) { // Collection response = { data: data.map(item => ({ type: req.baseUrl.split('/').pop() || 'resource', id: item.id, attributes: item })) }; } else if (data && typeof data === 'object') { // Single resource response = { data: { type: req.baseUrl.split('/').pop() || 'resource', id: data.id, attributes: data } }; } // Add links response.links = { self: `${req.protocol}://${req.get('host')}${req.originalUrl}` }; return originalJson.call(this, response); }; next(); }; } } module.exports = TransformMiddleware;
H. Error Handling Middleware Suite
src/middleware/error.middleware.js
/** * ADVANCED ERROR HANDLING MIDDLEWARE * Fitur: * - Multi-level error handling * - Error classification * - Graceful degradation * - Fallback responses * - Error reporting */ const logger = require('../utils/logger'); const config = require('../config/env'); class ErrorMiddleware { /** * 404 Not Found Handler */ static notFound() { return (req, res, next) => { const error = { name: 'NotFoundError', message: `Cannot ${req.method} ${req.originalUrl}`, code: 'NOT_FOUND', statusCode: 404 }; next(error); }; } /** * Database error handler */ static databaseError() { return (err, req, res, next) => { if (err.name === 'SequelizeError' || err.name === 'MongoError') { logger.error('Database error', { error: err.message, code: err.code, sql: err.sql, path: req.path }); return res.status(503).json({ success: false, error: 'DATABASE_ERROR', message: 'Database service unavailable', retryAfter: 30 }); } next(err); }; } /** * Validation error handler */ static validationError() { return (err, req, res, next) => { if (err.name === 'ValidationError') { return res.status(400).json({ success: false, error: 'VALIDATION_ERROR', message: err.message, errors: err.errors }); } next(err); }; } /** * Authentication error handler */ static authenticationError() { return (err, req, res, next) => { if (err.name === 'AuthenticationError' || err.name === 'JsonWebTokenError') { return res.status(err.statusCode || 401).json({ success: false, error: err.code || 'AUTHENTICATION_ERROR', message: err.message, ...(err.details && { details: err.details }) }); } next(err); }; } /** * Authorization error handler */ static authorizationError() { return (err, req, res, next) => { if (err.name === 'AuthorizationError') { return res.status(err.statusCode || 403).json({ success: false, error: err.code || 'FORBIDDEN', message: err.message, ...(err.details && { details: err.details }) }); } next(err); }; } /** * Rate limit error handler */ static rateLimitError() { return (err, req, res, next) => { if (err.name === 'RateLimitError') { return res.status(429).json({ success: false, error: 'RATE_LIMIT_EXCEEDED', message: err.message, retryAfter: err.retryAfter }); } next(err); }; } /** * Business logic error handler */ static businessError() { return (err, req, res, next) => { if (err.name === 'BusinessError') { return res.status(err.statusCode || 422).json({ success: false, error: err.code || 'BUSINESS_ERROR', message: err.message, ...(err.details && { details: err.details }) }); } next(err); }; } /** * Third-party service error handler */ static serviceError() { return (err, req, res, next) => { if (err.name === 'ServiceError') { logger.error('Third-party service error', { service: err.service, error: err.message, path: req.path }); return res.status(err.statusCode || 502).json({ success: false, error: 'SERVICE_ERROR', message: err.message || 'External service unavailable', service: err.service }); } next(err); }; } /** * Fallback: Graceful degradation */ static gracefulDegradation() { return (err, req, res, next) => { // If request is for critical endpoint, return degraded response if (req.path.startsWith('/api/critical')) { logger.warn('Graceful degradation activated', { path: req.path, error: err.message }); return res.status(200).json({ success: true, degraded: true, message: 'Service operating in degraded mode', data: null }); } next(err); }; } /** * Development error handler (detailed) */ static developmentError() { return (err, req, res, next) => { if (config.NODE_ENV === 'development') { logger.error('Development error handler', { error: err.stack, path: req.path }); return res.status(err.statusCode || 500).json({ success: false, error: err.code || 'INTERNAL_ERROR', message: err.message, stack: err.stack, details: err.details, path: req.path, method: req.method, timestamp: new Date().toISOString() }); } next(err); }; } /** * Production error handler (sanitized) */ static productionError() { return (err, req, res, next) => { // Log full error logger.error('Unhandled error', { error: err.message, stack: err.stack, path: req.path, method: req.method, userId: req.user?.id, requestId: req.requestId }); // Send sanitized response return res.status(500).json({ success: false, error: 'INTERNAL_SERVER_ERROR', message: 'An unexpected error occurred', requestId: req.requestId, timestamp: new Date().toISOString() }); }; } /** * Catch-all error handler */ static final() { return (err, req, res, next) => { // Ensure headers aren't already sent if (res.headersSent) { return next(err); } const statusCode = err.statusCode || 500; // Default error response const errorResponse = { success: false, error: err.code || 'INTERNAL_ERROR', message: err.message || 'Internal server error', requestId: req.requestId, timestamp: new Date().toISOString() }; // Add details in development if (config.NODE_ENV === 'development') { errorResponse.stack = err.stack; errorResponse.details = err.details; } res.status(statusCode).json(errorResponse); }; } } module.exports = ErrorMiddleware;
3.4 Middleware Execution Flow Control
/** * ADVANCED FLOW CONTROL DEMONSTRATION */ const express = require('express'); const app = express(); // ==================== 1. MULTIPLE MIDDLEWARE CHAINS ==================== app.use('/chain', // Middleware 1: Authentication (req, res, next) => { console.log('1. Auth middleware'); req.auth = { user: 'john', role: 'user' }; next(); }, // Middleware 2: Validation (req, res, next) => { console.log('2. Validation middleware'); if (!req.query.id) { return next(new Error('ID required')); } next(); }, // Middleware 3: Logging (req, res, next) => { console.log('3. Logging middleware'); console.log(`User: ${req.auth.user}, ID: ${req.query.id}`); next(); }, // Final Handler (req, res) => { console.log('4. Final handler'); res.json({ message: 'Chain completed' }); } ); // ==================== 2. CONDITIONAL MIDDLEWARE ==================== // Middleware that conditionally skips app.use((req, res, next) => { if (req.path === '/health') { return next(); // Skip to next middleware } console.log('Processing:', req.path); next(); }); // ==================== 3. MULTIPLE MIDDLEWARE PATHS ==================== // Array of middleware const authMiddleware = [ (req, res, next) => { console.log('Auth check 1'); next(); }, (req, res, next) => { console.log('Auth check 2'); req.isAuthenticated = true; next(); } ]; app.get('/secure', authMiddleware, (req, res) => { res.json({ message: 'Secure route' }); }); // ==================== 4. NEXT('ROUTE') SKIP REMAINING ==================== app.get('/skip', (req, res, next) => { console.log('Middleware 1'); if (req.query.skip) { return next('route'); // Skip to next route, not middleware } next(); }, (req, res, next) => { console.log('Middleware 2 - This will be skipped'); next(); }, (req, res) => { console.log('Handler 1'); res.json({ message: 'Regular route' }); } ); app.get('/skip', (req, res) => { console.log('Handler 2 - Skipped route'); res.json({ message: 'Skipped route' }); }); // ==================== 5. ASYNC MIDDLEWARE FLOW ==================== app.use(async (req, res, next) => { try { const start = Date.now(); // Simulate async operation await new Promise(resolve => setTimeout(resolve, 100)); console.log(`Async middleware took ${Date.now() - start}ms`); next(); } catch (error) { next(error); } }); // ==================== 6. DEPENDENT MIDDLEWARE ==================== // Middleware that depends on previous middleware app.use((req, res, next) => { req.timestamp = Date.now(); next(); }); app.use((req, res, next) => { req.requestId = `req_${req.timestamp}`; next(); }); app.use((req, res, next) => { console.log(`Request ID: ${req.requestId}`); next(); }); // ==================== 7. ERROR HANDLING FLOW ==================== app.get('/error-demo', (req, res, next) => { const error = new Error('Something went wrong'); error.statusCode = 400; error.code = 'DEMO_ERROR'; next(error); // Jump directly to error handler }); // Error handling middleware (4 parameters) app.use((err, req, res, next) => { console.error('Caught error:', err.message); // Can decide to pass to next error handler if (err.code === 'DEMO_ERROR') { return res.status(err.statusCode).json({ error: err.code, message: err.message }); } next(err); // Pass to next error handler }); // ==================== 8. MULTIPLE ERROR HANDLERS ==================== // Specific error handler for validation errors app.use((err, req, res, next) => { if (err.name === 'ValidationError') { return res.status(400).json({ error: 'Validation failed' }); } next(err); }); // Specific error handler for database errors app.use((err, req, res, next) => { if (err.name === 'DatabaseError') { return res.status(503).json({ error: 'Database unavailable' }); } next(err); }); // Catch-all error handler app.use((err, req, res, next) => { res.status(500).json({ error: 'Internal server error' }); });
3.5 Middleware Execution Order Visualization
/** * MIDDLEWARE EXECUTION ORDER VISUALIZATION * * Request masuk → [Middleware 1] → [Middleware 2] → [Middleware 3] → [Handler] * ↓ ↓ ↓ * (next) (next) (next) * ↓ ↓ ↓ * Error ← [Error Handler 3] ← [Error Handler 2] ← [Error Handler 1] * * Contoh konkret dengan timestamps: */ function demoMiddlewareOrder() { const express = require('express'); const app = express(); // Middleware 1: Logger app.use((req, res, next) => { console.log('1. Logger middleware - START'); console.log(` ${req.method} ${req.url}`); console.log(' Calling next()...'); next(); console.log('1. Logger middleware - END (response sent)'); }); // Middleware 2: Authenticator app.use((req, res, next) => { console.log('2. Auth middleware - START'); req.user = { id: 1, name: 'John' }; console.log(' User attached:', req.user); console.log(' Calling next()...'); next(); console.log('2. Auth middleware - END'); }); // Middleware 3: Validator app.use((req, res, next) => { console.log('3. Validator middleware - START'); if (!req.query.id) { console.log(' Validation failed!'); console.log(' Passing error to next()...'); return next(new Error('ID is required')); } console.log(' Validation passed'); console.log(' Calling next()...'); next(); console.log('3. Validator middleware - END'); }); // Route Handler app.get('/demo', (req, res) => { console.log('4. Route handler - START'); console.log(' Processing request...'); res.json({ message: 'Success', user: req.user, query: req.query }); console.log('4. Route handler - END (response sent)'); }); // Error Handler app.use((err, req, res, next) => { console.log('5. Error handler - START'); console.log(' Error caught:', err.message); console.log(' Sending error response...'); res.status(400).json({ error: err.message }); console.log('5. Error handler - END'); }); return app; } /** * OUTPUT - SUCCESS CASE (http://localhost:3000/demo?id=123): * * 1. Logger middleware - START * GET /demo?id=123 * Calling next()... * 2. Auth middleware - START * User attached: { id: 1, name: 'John' } * Calling next()... * 3. Validator middleware - START * Validation passed * Calling next()... * 4. Route handler - START * Processing request... * 4. Route handler - END (response sent) * 3. Validator middleware - END * 2. Auth middleware - END * 1. Logger middleware - END (response sent) * * OUTPUT - ERROR CASE (http://localhost:3000/demo): * * 1. Logger middleware - START * GET /demo * Calling next()... * 2. Auth middleware - START * User attached: { id: 1, name: 'John' } * Calling next()... * 3. Validator middleware - START * Validation failed! * Passing error to next()... * 5. Error handler - START * Error caught: ID is required * Sending error response... * 5. Error handler - END * 3. Validator middleware - END * 2. Auth middleware - END * 1. Logger middleware - END (response sent) */
3.6 Custom Middleware Patterns
1. Middleware Factory Pattern
/** * MIDDLEWARE FACTORY PATTERN * Create middleware with configuration */ function createRateLimiter(options = {}) { const { windowMs = 60000, max = 100, message = 'Too many requests' } = options; const requests = new Map(); return function rateLimiter(req, res, next) { const ip = req.ip; const now = Date.now(); // Get user's requests const userRequests = requests.get(ip) || []; // Clean old requests const recentRequests = userRequests.filter(time => now - time < windowMs); if (recentRequests.length >= max) { return res.status(429).json({ error: 'RATE_LIMIT_EXCEEDED', message, retryAfter: Math.ceil(windowMs / 1000) }); } // Add current request recentRequests.push(now); requests.set(ip, recentRequests); next(); }; } // Usage app.use('/api', createRateLimiter({ windowMs: 15 * 60 * 1000, max: 100, message: 'API rate limit exceeded' })); app.use('/auth', createRateLimiter({ windowMs: 15 * 60 * 1000, max: 5, message: 'Too many login attempts' }));
2. Middleware Chain Builder
/** * MIDDLEWARE CHAIN BUILDER PATTERN * Build complex middleware chains fluently */ class MiddlewareChain { constructor() { this.middlewares = []; } use(middleware) { this.middlewares.push(middleware); return this; } unless(condition, middleware) { return this.use((req, res, next) => { if (condition(req)) { return next(); } return middleware(req, res, next); }); } when(condition, middleware) { return this.use((req, res, next) => { if (condition(req)) { return middleware(req, res, next); } return next(); }); } build() { return this.middlewares; } } // Usage const apiMiddleware = new MiddlewareChain() .use(rateLimiter) .use(compression()) .when(req => req.method === 'POST', bodyParser.json()) .unless(req => req.path === '/health', authMiddleware) .build(); app.use('/api', apiMiddleware);
3. Middleware Composition
/** * MIDDLEWARE COMPOSITION PATTERN * Compose multiple middleware into one */ function compose(middlewares) { return function(req, res, next) { let index = 0; function dispatch(i) { if (i === middlewares.length) { return next(); } const middleware = middlewares[i]; try { middleware(req, res, (err) => { if (err) { return next(err); } dispatch(i + 1); }); } catch (err) { next(err); } } dispatch(0); }; } // Usage const authPipeline = compose([ extractToken, verifyToken, attachUser, logAuth ]); app.use('/secure', authPipeline);

0 Komentar