Middleware Dasar di Express.js - Pewira Learning Center


1. Latar Belakang

    Hari ke-empatbelas di Perwira Learning Center dan masih di Express.js kita akan Middleware Dasar. Midmembahas pembelajaran Express.js di Perwira Learning Center membahas middleware itu ibarat jalur produksi di pabrik:

  • Request = Bahan baku masuk
  • Middleware = Mesin-mesin di jalur produksi (pemeriksaan, pembersihan, pengolahan)
  • Response = Produk jadi keluar

Setiap middleware adalah "mesin" yang melakukan tugas spesifik sebelum request sampai ke handler utama. Tanpa middleware, kita harus menulis semua logika di setiap route!

2. Alat dan Bahan

a. Perangkat Lunak

  1. Express.js Project yang sudah berjalan
  2. Postman - Untuk testing middleware behavior
  3. VS Code - Untuk coding
  4. Node.js - Runtime environment

b. Perangkat Keras

  1. Laptop/PC standar

3. Pembahasan

3.1 Apa itu Middleware?

Middleware = Fungsi yang memiliki akses ke request object (req), response object (res), dan fungsi next() di request-response cycle.

Analogi Jalur Produksi:

text
📦 RAW MATERIAL (Request)
    ↓
🔧 MIDDLEWARE 1: Quality Check
    ↓
🔧 MIDDLEWARE 2: Cleaning
    ↓  
🔧 MIDDLEWARE 3: Processing
    ↓
🎯 FINAL HANDLER: Assembly
    ↓
📤 FINISHED PRODUCT (Response)

Karakteristik Middleware:

  1. Bisa mengubah req dan res object
  2. Bisa mengakhiri request-response cycle (dengan res.send())
  3. Bisa memanggil middleware berikutnya (dengan next())
  4. Dieksekusi berurutan sesuai urutan pendefinisian

3.2 Jenis-jenis Middleware

javascript
// 1. APPLICATION-LEVEL MIDDLEWARE
app.use((req, res, next) => {
  // Aplikasi-wide middleware
  next();
});

// 2. ROUTER-LEVEL MIDDLEWARE
router.use((req, res, next) => {
  // Hanya untuk router tertentu
  next();
});

// 3. ERROR-HANDLING MIDDLEWARE
app.use((err, req, res, next) => {
  // 4 parameter = error handler
  res.status(500).send('Error!');
});

// 4. BUILT-IN MIDDLEWARE
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// 5. THIRD-PARTY MIDDLEWARE
app.use(cors());
app.use(helmet());

3.3 Praktik Lengkap: Sistem dengan Berbagai Middleware

Buat file middleware-practice.js:

javascript
const express = require('express');
const app = express();
const PORT = 3006;

// ==================== 1. MIDDLEWARE DASAR ====================

// 🎯 MIDDLEWARE 1: Request Logger (Application-level)
app.use((req, res, next) => {
  const timestamp = new Date().toISOString();
  const method = req.method;
  const url = req.originalUrl;
  const ip = req.ip || req.connection.remoteAddress;
  
  console.log(`📝 [${timestamp}] ${method} ${url} from ${ip}`);
  
  // Tambahkan request ID untuk tracking
  req.requestId = `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  
  // Tambahkan start time untuk menghitung durasi
  req.startTime = Date.now();
  
  next(); // Lanjut ke middleware berikutnya
});

// 🎯 MIDDLEWARE 2: Security Headers
app.use((req, res, next) => {
  // Set security headers
  res.setHeader('X-Content-Type-Options', 'nosniff');
  res.setHeader('X-Frame-Options', 'DENY');
  res.setHeader('X-XSS-Protection', '1; mode=block');
  
  // Tambahkan custom header
  res.setHeader('X-Request-ID', req.requestId);
  
  next();
});

// 🎯 MIDDLEWARE 3: Body Parser (Built-in)
app.use(express.json()); // Parse JSON bodies
app.use(express.urlencoded({ extended: true })); // Parse URL-encoded bodies

// 🎯 MIDDLEWARE 4: CORS Handler (Custom)
app.use((req, res, next) => {
  // Simple CORS handling
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Request-ID');
  
  // Handle preflight requests
  if (req.method === 'OPTIONS') {
    return res.status(200).end();
  }
  
  next();
});

// ==================== 2. ROUTE-SPECIFIC MIDDLEWARE ====================

// 🔐 Middleware untuk Authentication
const requireAuth = (req, res, next) => {
  console.log(`🔐 Auth middleware dipanggil untuk: ${req.method} ${req.originalUrl}`);
  
  const authHeader = req.headers.authorization;
  
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({
      success: false,
      error: 'Unauthorized',
      message: 'Token authentication required',
      requestId: req.requestId
    });
  }
  
  // Simulasi token validation
  const token = authHeader.split(' ')[1];
  const validTokens = ['admin-token-123', 'user-token-456'];
  
  if (!validTokens.includes(token)) {
    return res.status(403).json({
      success: false,
      error: 'Forbidden',
      message: 'Invalid or expired token',
      requestId: req.requestId
    });
  }
  
  // Simpan user info di request object
  req.user = {
    id: token === 'admin-token-123' ? 1 : 2,
    role: token === 'admin-token-123' ? 'admin' : 'user',
    token: token
  };
  
  console.log(`✅ User authenticated: ${req.user.role} (ID: ${req.user.id})`);
  next();
};

// 👑 Middleware untuk Authorization (Admin only)
const requireAdmin = (req, res, next) => {
  console.log(`👑 Admin check middleware dipanggil`);
  
  if (!req.user) {
    return res.status(401).json({
      success: false,
      error: 'Unauthorized',
      message: 'Authentication required first'
    });
  }
  
  if (req.user.role !== 'admin') {
    return res.status(403).json({
      success: false,
      error: 'Forbidden',
      message: 'Admin privileges required',
      userRole: req.user.role,
      requiredRole: 'admin'
    });
  }
  
  console.log(`✅ Admin access granted for user ${req.user.id}`);
  next();
};

// 📊 Middleware untuk Rate Limiting (Simple version)
const requestCounts = new Map();
const rateLimiter = (req, res, next) => {
  const ip = req.ip || 'unknown';
  const now = Date.now();
  const windowMs = 60 * 1000; // 1 minute
  const maxRequests = 10;
  
  // Clean old entries
  const userRequests = requestCounts.get(ip) || [];
  const recentRequests = userRequests.filter(time => now - time < windowMs);
  
  if (recentRequests.length >= maxRequests) {
    return res.status(429).json({
      success: false,
      error: 'Too Many Requests',
      message: 'Rate limit exceeded',
      retryAfter: Math.ceil((recentRequests[0] + windowMs - now) / 1000),
      limit: `${maxRequests} requests per minute`
    });
  }
  
  // Add current request
  recentRequests.push(now);
  requestCounts.set(ip, recentRequests);
  
  // Add headers
  res.setHeader('X-RateLimit-Limit', maxRequests);
  res.setHeader('X-RateLimit-Remaining', maxRequests - recentRequests.length);
  res.setHeader('X-RateLimit-Reset', Math.ceil((recentRequests[0] + windowMs) / 1000));
  
  next();
};

// ==================== 3. LOGGING MIDDLEWARE ====================

// Request Logging Middleware (dengan warna!)
const coloredLogger = (req, res, next) => {
  const originalSend = res.send;
  const startTime = Date.now();
  
  // Log request
  console.log(`➡️  ${getColoredMethod(req.method)} ${req.originalUrl}`);
  
  // Override res.send untuk log response
  res.send = function(body) {
    const duration = Date.now() - startTime;
    const statusCode = res.statusCode;
    const statusColor = getStatusColor(statusCode);
    
    console.log(`⬅️  ${statusColor}${statusCode}\x1b[0m ${req.method} ${req.originalUrl} - ${duration}ms`);
    
    // Log response body untuk debugging (hanya di development)
    if (process.env.NODE_ENV === 'development' && body) {
      try {
        const parsedBody = typeof body === 'string' ? JSON.parse(body) : body;
        console.log(`   Response:`, JSON.stringify(parsedBody, null, 2).substring(0, 200) + '...');
      } catch {
        console.log(`   Response: ${typeof body === 'string' ? body.substring(0, 100) : 'binary data'}`);
      }
    }
    
    return originalSend.call(this, body);
  };
  
  next();
};

// Helper functions untuk warna
function getColoredMethod(method) {
  const colors = {
    GET: '\x1b[32m',    // hijau
    POST: '\x1b[34m',   // biru
    PUT: '\x1b[33m',    // kuning
    DELETE: '\x1b[31m', // merah
    PATCH: '\x1b[35m'   // magenta
  };
  return `${colors[method] || '\x1b[37m'}${method}\x1b[0m`;
}

function getStatusColor(statusCode) {
  if (statusCode >= 500) return '\x1b[31m'; // merah untuk server error
  if (statusCode >= 400) return '\x1b[33m'; // kuning untuk client error
  if (statusCode >= 300) return '\x1b[36m'; // cyan untuk redirect
  return '\x1b[32m'; // hijau untuk success
}

// Gunakan logging middleware
app.use(coloredLogger);

// ==================== 4. DATA TRANSFORMATION MIDDLEWARE ====================

// Middleware untuk transformasi request data
const requestTransformer = (req, res, next) => {
  console.log('🔄 Request Transformer Middleware');
  
  // 1. Trim semua string fields
  if (req.body && typeof req.body === 'object') {
    Object.keys(req.body).forEach(key => {
      if (typeof req.body[key] === 'string') {
        req.body[key] = req.body[key].trim();
      }
    });
  }
  
  // 2. Convert string numbers to actual numbers
  if (req.query && typeof req.query === 'object') {
    Object.keys(req.query).forEach(key => {
      const value = req.query[key];
      if (!isNaN(value) && value.trim() !== '') {
        req.query[key] = Number(value);
      }
    });
  }
  
  // 3. Add metadata
  req.metadata = {
    transformed: true,
    timestamp: new Date().toISOString(),
    contentType: req.headers['content-type'] || 'unknown'
  };
  
  next();
};

// Middleware untuk transformasi response data
const responseTransformer = (req, res, next) => {
  const originalJson = res.json;
  
  res.json = function(data) {
    // Transform response structure
    const transformedData = {
      success: true,
      data: data,
      metadata: {
        requestId: req.requestId,
        timestamp: new Date().toISOString(),
        duration: Date.now() - (req.startTime || Date.now()),
        ...req.metadata
      }
    };
    
    return originalJson.call(this, transformedData);
  };
  
  next();
};

// Gunakan transformer middleware
app.use(requestTransformer);
app.use(responseTransformer);

// ==================== 5. ROUTES DENGAN BERBAGAI MIDDLEWARE ====================

// 🏠 PUBLIC ROUTE (tanpa authentication)
app.get('/', (req, res) => {
  res.json({
    message: 'Welcome to Middleware Practice API!',
    endpoints: {
      public: {
        '/': 'This page',
        '/api/public': 'Public data',
        '/api/register': 'Register new user (POST)'
      },
      protected: {
        '/api/profile': 'Get user profile (requires auth)',
        '/api/admin': 'Admin panel (requires admin)',
        '/api/limited': 'Rate limited endpoint'
      }
    },
    requestInfo: {
      id: req.requestId,
      ip: req.ip,
      userAgent: req.headers['user-agent'],
      metadata: req.metadata
    }
  });
});

// 🌐 PUBLIC API
app.get('/api/public', (req, res) => {
  res.json({
    message: 'This is public data',
    serverTime: new Date().toISOString(),
    stats: {
      totalRequests: Array.from(requestCounts.values()).reduce((sum, arr) => sum + arr.length, 0),
      uniqueIPs: requestCounts.size
    }
  });
});

// 📝 REGISTER (public but with rate limiting)
app.post('/api/register', rateLimiter, (req, res) => {
  const { name, email } = req.body;
  
  if (!name || !email) {
    return res.status(400).json({
      success: false,
      error: 'Validation Error',
      message: 'Name and email are required'
    });
  }
  
  // Simulasi user creation
  const newUser = {
    id: Date.now(),
    name,
    email,
    registeredAt: new Date().toISOString(),
    requestId: req.requestId
  };
  
  res.status(201).json(newUser);
});

// 🔐 PROTECTED ROUTE (requires authentication)
app.get('/api/profile', requireAuth, (req, res) => {
  res.json({
    message: 'Welcome to your profile!',
    user: req.user,
    profile: {
      name: 'John Doe',
      email: 'john@example.com',
      joined: '2024-01-01',
      lastLogin: new Date().toISOString()
    },
    recentActivity: [
      { action: 'Login', time: '10:30 AM' },
      { action: 'Viewed profile', time: '10:31 AM' }
    ]
  });
});

// 👑 ADMIN ROUTE (requires admin role)
app.get('/api/admin', requireAuth, requireAdmin, (req, res) => {
  res.json({
    message: 'Welcome to Admin Panel!',
    user: req.user,
    adminFeatures: [
      'View all users',
      'Manage permissions',
      'System configuration',
      'View analytics'
    ],
    systemStats: {
      totalUsers: 150,
      activeUsers: 89,
      serverUptime: '99.9%',
      memoryUsage: '45%'
    }
  });
});

// 🚫 RATE LIMITED ROUTE
app.get('/api/limited', rateLimiter, (req, res) => {
  res.json({
    message: 'This endpoint is rate limited',
    note: 'You can only call this 10 times per minute',
    yourIP: req.ip,
    remainingRequests: res.getHeader('X-RateLimit-Remaining')
  });
});

// 🔗 MIDDLEWARE CHAINING EXAMPLE
app.get('/api/chain', 
  // Middleware 1: Logging
  (req, res, next) => {
    console.log('🔗 Middleware 1: Logging');
    req.chainStep = 1;
    next();
  },
  
  // Middleware 2: Validation
  (req, res, next) => {
    console.log('🔗 Middleware 2: Validation');
    req.chainStep = 2;
    
    // Cek query parameter
    if (!req.query.apiKey) {
      return res.status(400).json({
        error: 'Missing apiKey query parameter'
      });
    }
    next();
  },
  
  // Middleware 3: Processing
  (req, res, next) => {
    console.log('🔗 Middleware 3: Processing');
    req.chainStep = 3;
    req.processedData = {
      originalQuery: req.query,
      processedAt: new Date().toISOString()
    };
    next();
  },
  
  // Final Handler
  (req, res) => {
    console.log('🎯 Final Handler');
    res.json({
      message: 'Middleware chain completed successfully!',
      steps: req.chainStep,
      processedData: req.processedData,
      requestId: req.requestId
    });
  }
);

// ==================== 6. ERROR HANDLING MIDDLEWARE ====================

// Custom error class untuk middleware
class MiddlewareError extends Error {
  constructor(message, statusCode = 500) {
    super(message);
    this.statusCode = statusCode;
    this.name = 'MiddlewareError';
  }
}

// Error simulation middleware
app.get('/api/error/simulate', (req, res, next) => {
  const errorType = req.query.type || 'generic';
  
  switch(errorType) {
    case 'validation':
      const error = new MiddlewareError('Validation failed', 400);
      error.details = { field: 'email', reason: 'Invalid format' };
      return next(error);
      
    case 'database':
      return next(new Error('Database connection failed'));
      
    case 'async':
      // Simulasi async error
      setTimeout(() => {
        next(new Error('Async operation failed'));
      }, 100);
      break;
      
    default:
      throw new Error('Generic error simulation');
  }
});

// Global error handling middleware (harus punya 4 parameter)
app.use((err, req, res, next) => {
  console.error('🔥 Error Handler Middleware:', err);
  
  const statusCode = err.statusCode || 500;
  const errorResponse = {
    success: false,
    error: {
      type: err.name || 'InternalServerError',
      message: err.message || 'Something went wrong',
      statusCode: statusCode,
      timestamp: new Date().toISOString(),
      requestId: req.requestId,
      path: req.originalUrl
    }
  };
  
  // Tambahkan details jika ada
  if (err.details) {
    errorResponse.error.details = err.details;
  }
  
  // Stack trace hanya di development
  if (process.env.NODE_ENV === 'development') {
    errorResponse.error.stack = err.stack;
  }
  
  res.status(statusCode).json(errorResponse);
});

// ==================== 7. ASYNC MIDDLEWARE ====================

// Async middleware example
const asyncMiddleware = fn => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};

// Database simulation
const mockDatabase = {
  findUser: (id) => {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        if (id === 1) {
          resolve({ id: 1, name: 'John Doe', role: 'admin' });
        } else if (id === 2) {
          resolve({ id: 2, name: 'Jane Smith', role: 'user' });
        } else {
          reject(new Error('User not found'));
        }
      }, 100);
    });
  }
};

// Middleware async untuk fetch user data
const fetchUserData = asyncMiddleware(async (req, res, next) => {
  const userId = parseInt(req.query.userId);
  
  if (!userId) {
    throw new MiddlewareError('userId query parameter is required', 400);
  }
  
  const user = await mockDatabase.findUser(userId);
  req.userData = user;
  next();
});

// Route dengan async middleware
app.get('/api/user', fetchUserData, (req, res) => {
  res.json({
    success: true,
    message: 'User data retrieved',
    data: req.userData,
    fetchedAt: new Date().toISOString()
  });
});

// ==================== 8. CONDITIONAL MIDDLEWARE ====================

// Middleware yang hanya berjalan di kondisi tertentu
const developmentOnly = (req, res, next) => {
  if (process.env.NODE_ENV === 'development') {
    console.log('🔧 Development middleware active');
    req.isDevelopment = true;
    
    // Tambahkan debug info
    res.setHeader('X-Debug-Mode', 'enabled');
    res.setHeader('X-Node-Env', process.env.NODE_ENV);
  }
  next();
};

// Middleware berdasarkan path
const apiLogger = (paths) => {
  return (req, res, next) => {
    if (paths.some(path => req.originalUrl.startsWith(path))) {
      console.log(`📊 API Logger: ${req.method} ${req.originalUrl}`);
    }
    next();
  };
};

// Gunakan conditional middleware
app.use(developmentOnly);
app.use(apiLogger(['/api/admin', '/api/profile']));

// ==================== 9. THIRD-PARTY MIDDLEWARE SIMULATION ====================

// Simulasi helmet.js (security middleware)
const securityMiddleware = (req, res, next) => {
  console.log('🛡️ Security middleware activated');
  
  // Remove potentially sensitive headers
  res.removeHeader('X-Powered-By');
  
  // Add security headers
  const securityHeaders = {
    'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
    'Content-Security-Policy': "default-src 'self'",
    'X-Content-Type-Options': 'nosniff',
    'Referrer-Policy': 'strict-origin-when-cross-origin'
  };
  
  Object.entries(securityHeaders).forEach(([key, value]) => {
    res.setHeader(key, value);
  });
  
  next();
};

// Simulasi morgan (logging middleware)
const morganStyleLogger = (req, res, next) => {
  const start = Date.now();
  
  res.on('finish', () => {
    const duration = Date.now() - start;
    const status = res.statusCode;
    const length = res.get('Content-Length') || 0;
    
    console.log(`${req.method} ${req.originalUrl} ${status} ${length} - ${duration}ms`);
  });
  
  next();
};

// Gunakan third-party style middleware
app.use(securityMiddleware);
app.use(morganStyleLogger);

// ==================== 10. MIDDLEWARE ORDER DEMONSTRATION ====================

// Demonstrasi pentingnya urutan middleware
app.get('/api/order-demo',
  // Middleware A: Tambahkan property
  (req, res, next) => {
    console.log('Middleware A executed');
    req.demo = 'A';
    next();
  },
  
  // Middleware B: Modify property
  (req, res, next) => {
    console.log('Middleware B executed');
    req.demo += ' → B';
    next();
  },
  
  // Middleware C: Async operation
  asyncMiddleware(async (req, res, next) => {
    console.log('Middleware C (async) executed');
    await new Promise(resolve => setTimeout(resolve, 50));
    req.demo += ' → C';
    next();
  }),
  
  // Final handler
  (req, res) => {
    res.json({
      message: 'Middleware order demonstration',
      executionOrder: req.demo,
      explanation: 'Middleware dieksekusi berurutan dari atas ke bawah',
      currentTime: new Date().toISOString()
    });
  }
);

// ==================== 404 HANDLER (Middleware terakhir) ====================
app.use('*', (req, res) => {
  res.status(404).json({
    success: false,
    error: 'Not Found',
    message: `Cannot ${req.method} ${req.originalUrl}`,
    requestId: req.requestId,
    suggestion: 'Check the available endpoints at GET /'
  });
});

// ==================== RESPONSE TIME MIDDLEWARE ====================
// Middleware untuk mengukur response time
app.use((req, res, next) => {
  const start = process.hrtime();
  
  res.on('finish', () => {
    const diff = process.hrtime(start);
    const responseTime = diff[0] * 1e3 + diff[1] * 1e-6; // dalam ms
    
    // Log response time yang lambat
    if (responseTime > 1000) { // lebih dari 1 detik
      console.warn(`🐢 Slow response: ${req.method} ${req.originalUrl} took ${responseTime.toFixed(2)}ms`);
    }
    
    // Tambahkan header
    res.setHeader('X-Response-Time', `${responseTime.toFixed(2)}ms`);
  });
  
  next();
});

// ==================== START SERVER ====================
app.listen(PORT, () => {
  console.log("=".repeat(70));
  console.log("⚙️  MIDDLEWARE PRACTICE SERVER");
  console.log("=".repeat(70));
  console.log(`🌐 Server: http://localhost:${PORT}`);
  console.log(`🔧 Mode: ${process.env.NODE_ENV || 'development'}`);
  console.log("");
  console.log("🎯 COBA MIDDLEWARE BERIKUT:");
  console.log("");
  console.log("1. Logging Middleware:");
  console.log("   GET / → Lihat log warna-warni di terminal");
  console.log("");
  console.log("2. Authentication Middleware:");
  console.log("   GET /api/profile → 401 Unauthorized");
  console.log("   GET /api/profile");
  console.log("   Headers: { Authorization: Bearer user-token-456 }");
  console.log("");
  console.log("3. Authorization Middleware:");
  console.log("   GET /api/admin");
  console.log("   Headers: { Authorization: Bearer user-token-456 } → 403 Forbidden");
  console.log("   Headers: { Authorization: Bearer admin-token-123 } → Success");
  console.log("");
  console.log("4. Rate Limiting Middleware:");
  console.log("   GET /api/limited → Refresh cepat untuk lihat 429 error");
  console.log("");
  console.log("5. Middleware Chaining:");
  console.log("   GET /api/chain → Tanpa query");
  console.log("   GET /api/chain?apiKey=123 → Dengan query");
  console.log("");
  console.log("6. Error Handling Middleware:");
  console.log("   GET /api/error/simulate?type=validation");
  console.log("   GET /api/error/simulate?type=database");
  console.log("");
  console.log("7. Async Middleware:");
  console.log("   GET /api/user → Error (no userId)");
  console.log("   GET /api/user?userId=1 → Success");
  console.log("   GET /api/user?userId=999 → Error (not found)");
  console.log("");
  console.log("8. Middleware Order:");
  console.log("   GET /api/order-demo → Lihat urutan eksekusi");
  console.log("=".repeat(70));
});

3.4 Testing Middleware dengan Postman

Test 1: Authentication Middleware

http
# Tanpa token → 401 Unauthorized
GET http://localhost:3006/api/profile

# Dengan user token → Success
GET http://localhost:3006/api/profile
Authorization: Bearer user-token-456

# Dengan admin token → Success
GET http://localhost:3006/api/profile
Authorization: Bearer admin-token-123

Test 2: Authorization Middleware

http
# User mencoba akses admin → 403 Forbidden
GET http://localhost:3006/api/admin
Authorization: Bearer user-token-456

# Admin akses admin panel → Success
GET http://localhost:3006/api/admin
Authorization: Bearer admin-token-123

Test 3: Rate Limiting

http
# Coba refresh 10+ kali dalam 1 menit
GET http://localhost:3006/api/limited

Test 4: Middleware Chain

http
# Tanpa apiKey → Error
GET http://localhost:3006/api/chain

# Dengan apiKey → Success
GET http://localhost:3006/api/chain?apiKey=123

3.5 Middleware Flow Control

javascript
// Middleware dengan conditional next()
app.use((req, res, next) => {
  console.log('Middleware 1');
  
  // Skip ke error handler jika ada error
  if (req.query.error) {
    return next(new Error('Simulated error'));
  }
  
  // Skip ke route berikutnya jika condition true
  if (req.query.skip) {
    return next('route'); // Skip ke route handler berikutnya
  }
  
  // Continue ke middleware berikutnya
  next();
});

// Middleware yang hanya berjalan untuk path tertentu
app.use('/admin', (req, res, next) => {
  console.log('Admin middleware hanya untuk /admin routes');
  next();
});

// Multiple middleware untuk route tertentu
app.get('/secure',
  middleware1,
  middleware2,
  middleware3,
  (req, res) => {
    res.send('Secure route');
  }
);

3.6 Best Practices Middleware

1. Order Matters!

javascript
// ❌ Wrong order
app.use(errorHandler); // Error handler harus di akhir!
app.use(authMiddleware);
app.get('/api/data', handler);

// ✅ Correct order
app.use(authMiddleware); // Auth dulu
app.get('/api/data', handler); // Route
app.use(errorHandler); // Error handler di akhir

2. Use app.use() vs app.METHOD()

javascript
// app.use() → Untuk semua HTTP methods
app.use('/api', apiMiddleware); // GET, POST, PUT, DELETE semua kena

// app.METHOD() → Untuk method spesifik
app.get('/api/data', getMiddleware); // Hanya GET
app.post('/api/data', postMiddleware); // Hanya POST

3. Middleware Factory Pattern

javascript
// Middleware factory untuk konfigurasi
const createLogger = (options = {}) => {
  return (req, res, next) => {
    const shouldLog = options.enabled !== false;
    
    if (shouldLog) {
      console.log(`[${options.name || 'Logger'}] ${req.method} ${req.url}`);
    }
    
    if (options.addTimestamp) {
      req.timestamp = new Date().toISOString();
    }
    
    next();
  };
};

// Gunakan dengan konfigurasi
app.use(createLogger({ name: 'API', enabled: true, addTimestamp: true }));

4. Error Handling di Async Middleware

javascript
// ❌ Wrong (error tidak tertangkap)
app.use(async (req, res, next) => {
  const data = await fetchData(); // Jika error, crash!
  next();
});

// ✅ Correct (gunakan try-catch atau wrapper)
app.use(async (req, res, next) => {
  try {
    const data = await fetchData();
    next();
  } catch (error) {
    next(error); // Pass error ke error handler
  }
});

// Atau gunakan wrapper function
const asyncMiddleware = (fn) => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};

app.use(asyncMiddleware(async (req, res, next) => {
  const data = await fetchData();
  next();
}));

3.7 Common Third-Party Middleware

javascript
// Popular third-party middleware
const express = require('express');
const helmet = require('helmet'); // Security headers
const cors = require('cors'); // CORS handling
const morgan = require('morgan'); // Logging
const compression = require('compression'); // Response compression
const cookieParser = require('cookie-parser'); // Cookie parsing

const app = express();

// Basic setup dengan third-party middleware
app.use(helmet()); // Security
app.use(cors()); // CORS
app.use(compression()); // Compression
app.use(morgan('combined')); // Logging
app.use(cookieParser()); // Cookies
app.use(express.json()); // Body parsing

// Custom middleware setelah third-party
app.use(myCustomMiddleware);

3.8 Middleware Performance Tips

javascript
// 1. Skip middleware untuk static files
app.use('/public', express.static('public'));

// 2. Conditional middleware loading
if (process.env.NODE_ENV === 'development') {
  app.use(require('morgan')('dev')); // Logging hanya di dev
}

// 3. Cache expensive operations
const cache = new Map();
app.use((req, res, next) => {
  const cacheKey = req.originalUrl;
  
  if (cache.has(cacheKey)) {
    return res.json(cache.get(cacheKey));
  }
  
  // Override res.json untuk caching
  const originalJson = res.json;
  res.json = function(data) {
    cache.set(cacheKey, data);
    setTimeout(() => cache.delete(cacheKey), 60000); // 1 minute TTL
    return originalJson.call(this, data);
  };
  
  next();
});

3.9 Testing Middleware

javascript
// Unit test untuk middleware
const testMiddleware = require('./middleware');
const request = require('supertest');
const express = require('express');

describe('Authentication Middleware', () => {
  it('should return 401 without token', async () => {
    const app = express();
    app.use(testMiddleware.requireAuth);
    app.get('/test', (req, res) => res.send('OK'));
    
    const response = await request(app).get('/test');
    expect(response.status).toBe(401);
  });
  
  it('should call next() with valid token', async () => {
    const app = express();
    app.use(testMiddleware.requireAuth);
    app.get('/test', (req, res) => res.json({ user: req.user }));
    
    const response = await request(app)
      .get('/test')
      .set('Authorization', 'Bearer valid-token');
    
    expect(response.status).toBe(200);
    expect(response.body.user).toBeDefined();
  });
});

3.10 Latihan Praktikum

Exercise 1: Audit Logging Middleware

javascript
// Buat middleware untuk audit logging:
// 1. Log semua aktivitas user (login, logout, data changes)
// 2. Simpan ke file/database
// 3. Include: timestamp, user ID, action, IP address
// 4. Skip logging untuk request tertentu (health checks)

Exercise 2: Request Validation Chain

javascript
// Buat middleware chain untuk validasi:
// 1. Schema validation (format data)
// 2. Business rule validation
// 3. Permission validation
// 4. Rate limit validation
// 5. Jika semua lolos, baru execute handler

Exercise 3: Feature Flag Middleware

javascript
// Buat middleware untuk feature flags:
// 1. Baca feature flags dari config
// 2. Enable/disable routes berdasarkan flag
// 3. A/B testing support
// 4. Rollout percentage (10% users get new feature)

4. Daftar Pustaka

  1. Express.js Documentation (n.d.). Using Middlewarehttps://expressjs.com
  2. Codepolitan (2023). Menerapkan Middleware dalam Express.js untuk Pengembangan Web yang Efisienhttps://www.codepolitan.com
  3. Express.js Documentation (n.d.). Middleare Cors. https://expressjs.com
  4. GeeksForGeeks (2026). Middleware in Expresshttps://www.geeksforgeeks.org
  5. Scaler (2024). Builtin MiddleWar in ExpressJS. https://www.scaler.com

Posting Komentar

0 Komentar