1. Latar Belakang
Oke kita lanjuut dan masih di Express.js, kita akan membahas Validasi Input dan Error Handling. Ini ibarat sistem keamanan dan pemadam kebakaran di restoran:
- Validasi = Pemeriksaan kualitas bahan makanan sebelum dimasak
- Error Handling = Prosedur darurat jika terjadi masalah di dapur
Tanpa validasi, server kita bisa menerima data beracun. Tanpa error handling, satu error kecil bisa merobohkan seluruh sistem!
2. Alat dan Bahan
a. Perangkat Lunak
- Express.js Project yang sudah berjalan
- Postman - Untuk testing error scenarios
- VS Code - Untuk coding
- Node.js - Runtime environment
b. Perangkat Keras
- Laptop/PC standar
3. Pembahasan
3.1 Kenapa Validasi dan Error Handling Penting?
Masalah tanpa validasi:
// ❌ Tanpa validasi - BAHAYA! app.post('/api/users', (req, res) => { const user = req.body; database.save(user); // Langsung save, gak dicek! res.json({ success: true }); }); // Client bisa kirim: // { // "name": "", // nama kosong // "email": "bukan-email", // email invalid // "age": -5, // umur minus // "password": "123" // password terlalu pendek // }
Dampak buruk:
- Data kotor di database
- Security vulnerability (SQL injection, XSS)
- Aplikasi crash karena data tidak sesuai ekspektasi
- User experience buruk - error message tidak jelas
3.2 Praktik Lengkap: Sistem Registrasi dengan Validasi Komprehensif
Buat file validation-error-practice.js:
const express = require('express'); const app = express(); const PORT = 3005; // Middleware app.use(express.json()); // Helper untuk response error yang konsisten const errorResponse = (statusCode, errorType, message, details = null) => { return { success: false, error: { type: errorType, message: message, code: statusCode, timestamp: new Date().toISOString(), details: details } }; }; // Helper untuk response success yang konsisten const successResponse = (message, data = null, metadata = null) => { return { success: true, message: message, data: data, metadata: metadata, timestamp: new Date().toISOString() }; }; // ==================== VALIDASI UTILITIES ==================== const ValidationUtils = { // Validasi email isValidEmail: (email) => { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); }, // Validasi phone number Indonesia isValidPhone: (phone) => { const phoneRegex = /^(?:\+62|62|0)[2-9][0-9]{7,11}$/; return phoneRegex.test(phone.replace(/\s+/g, '')); }, // Validasi password strength isStrongPassword: (password) => { const minLength = 8; const hasUpperCase = /[A-Z]/.test(password); const hasLowerCase = /[a-z]/.test(password); const hasNumbers = /\d/.test(password); const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(password); return password.length >= minLength && hasUpperCase && hasLowerCase && hasNumbers && hasSpecialChar; }, // Validasi tanggal (tidak di masa lalu) isValidFutureDate: (dateString) => { const inputDate = new Date(dateString); const today = new Date(); today.setHours(0, 0, 0, 0); return inputDate >= today; }, // Validasi URL isValidURL: (url) => { try { new URL(url); return true; } catch { return false; } }, // Sanitize input (basic XSS prevention) sanitizeInput: (input) => { if (typeof input !== 'string') return input; return input .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, ''') .replace(/\//g, '/'); } }; // ==================== VALIDASI MIDDLEWARE ==================== // Middleware untuk validasi user registration const validateUserRegistration = (req, res, next) => { console.log('🔍 Validasi data user registration:', req.body); const { name, email, password, phone, birthDate } = req.body; const errors = []; const warnings = []; const sanitizedData = {}; // 1. Validasi NAME if (!name) { errors.push({ field: 'name', message: 'Nama wajib diisi', code: 'REQUIRED_FIELD' }); } else if (name.length < 3) { errors.push({ field: 'name', message: 'Nama minimal 3 karakter', code: 'MIN_LENGTH', min: 3, current: name.length }); } else if (name.length > 100) { errors.push({ field: 'name', message: 'Nama maksimal 100 karakter', code: 'MAX_LENGTH', max: 100, current: name.length }); } else { sanitizedData.name = ValidationUtils.sanitizeInput(name.trim()); } // 2. Validasi EMAIL if (!email) { errors.push({ field: 'email', message: 'Email wajib diisi', code: 'REQUIRED_FIELD' }); } else if (!ValidationUtils.isValidEmail(email)) { errors.push({ field: 'email', message: 'Format email tidak valid', code: 'INVALID_FORMAT', example: 'user@example.com', received: email }); } else { sanitizedData.email = email.toLowerCase().trim(); } // 3. Validasi PASSWORD if (!password) { errors.push({ field: 'password', message: 'Password wajib diisi', code: 'REQUIRED_FIELD' }); } else { const passwordStrength = []; if (password.length < 8) { passwordStrength.push('minimal 8 karakter'); } if (!/[A-Z]/.test(password)) { passwordStrength.push('minimal 1 huruf besar'); } if (!/[a-z]/.test(password)) { passwordStrength.push('minimal 1 huruf kecil'); } if (!/\d/.test(password)) { passwordStrength.push('minimal 1 angka'); } if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) { passwordStrength.push('minimal 1 karakter khusus'); } if (passwordStrength.length > 0) { errors.push({ field: 'password', message: 'Password terlalu lemah', code: 'WEAK_PASSWORD', requirements: passwordStrength, suggestion: 'Gunakan kombinasi huruf besar, kecil, angka, dan karakter khusus' }); } else { sanitizedData.password = password; // Dalam real app, ini akan di-hash } } // 4. Validasi PHONE (opsional) if (phone && !ValidationUtils.isValidPhone(phone)) { warnings.push({ field: 'phone', message: 'Format nomor telepon mungkin tidak valid', code: 'POTENTIAL_INVALID_PHONE', format: 'Contoh: +6281234567890 atau 081234567890' }); sanitizedData.phone = phone.replace(/\s+/g, ''); } else if (phone) { sanitizedData.phone = phone.replace(/\s+/g, ''); } // 5. Validasi BIRTH DATE (opsional) if (birthDate) { const birthDateObj = new Date(birthDate); const today = new Date(); const minAge = 13; // Minimal 13 tahun const maxAge = 120; // Maksimal 120 tahun const age = today.getFullYear() - birthDateObj.getFullYear(); if (isNaN(birthDateObj.getTime())) { errors.push({ field: 'birthDate', message: 'Format tanggal lahir tidak valid', code: 'INVALID_DATE_FORMAT', format: 'YYYY-MM-DD', received: birthDate }); } else if (age < minAge) { errors.push({ field: 'birthDate', message: `Minimal berusia ${minAge} tahun`, code: 'MIN_AGE_REQUIRED', minAge: minAge, calculatedAge: age }); } else if (age > maxAge) { warnings.push({ field: 'birthDate', message: `Usia terlihat tidak wajar (lebih dari ${maxAge} tahun)`, code: 'POTENTIAL_INVALID_AGE', calculatedAge: age }); sanitizedData.birthDate = birthDate; } else { sanitizedData.birthDate = birthDate; } } // 6. Simpan hasil validasi di request object req.validation = { errors, warnings, sanitizedData, hasErrors: errors.length > 0, hasWarnings: warnings.length > 0 }; console.log('📊 Hasil validasi:', req.validation); // Jika ada errors, kembalikan error response if (req.validation.hasErrors) { return res.status(400).json( errorResponse( 400, 'VALIDATION_ERROR', 'Validasi data gagal', { errors: req.validation.errors, warnings: req.validation.warnings, totalErrors: req.validation.errors.length, totalWarnings: req.validation.warnings.length } ) ); } // Jika hanya warnings, lanjutkan dengan warning if (req.validation.hasWarnings) { console.log('⚠️ Ada warnings, tapi lanjut proses:', warnings); } // Replace req.body dengan data yang sudah disanitasi req.body = sanitizedData; next(); }; // Middleware untuk validasi login const validateLogin = (req, res, next) => { const { email, password } = req.body; const errors = []; if (!email) { errors.push({ field: 'email', message: 'Email wajib diisi', code: 'REQUIRED_FIELD' }); } if (!password) { errors.push({ field: 'password', message: 'Password wajib diisi', code: 'REQUIRED_FIELD' }); } if (errors.length > 0) { return res.status(400).json( errorResponse( 400, 'VALIDATION_ERROR', 'Data login tidak lengkap', { errors } ) ); } next(); }; // ==================== DATABASE SIMULASI ==================== const users = []; let userId = 1; // Simulasi async database operation yang bisa error const simulatedDatabase = { saveUser: (userData) => { return new Promise((resolve, reject) => { // Simulasi: 10% chance database error if (Math.random() < 0.1) { reject(new Error('Database connection failed')); return; } // Simulasi: cek duplicate email const emailExists = users.some(u => u.email === userData.email); if (emailExists) { reject({ name: 'DuplicateError', message: 'Email already registered', code: 'EMAIL_EXISTS', field: 'email' }); return; } // Simulasi: save ke "database" const newUser = { id: userId++, ...userData, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), isActive: true }; users.push(newUser); // Simulasi: delay database operation setTimeout(() => { resolve(newUser); }, 100); }); }, findUserByEmail: (email) => { return new Promise((resolve, reject) => { // Simulasi: 5% chance database error if (Math.random() < 0.05) { reject(new Error('Database query failed')); return; } const user = users.find(u => u.email === email); setTimeout(() => { resolve(user || null); }, 50); }); } }; // ==================== ROUTES DENGAN VALIDASI ==================== // 🏠 HOME ROUTE app.get('/', (req, res) => { res.json( successResponse( '🔐 Validation & Error Handling Practice API', { endpoints: { register: 'POST /api/register (dengan validasi ketat)', login: 'POST /api/login (dengan error handling)', users: 'GET /api/users', errorDemo: 'GET /api/error/demo/:type (demo berbagai error)' }, currentStats: { totalUsers: users.length, apiStatus: 'operational' } } ) ); }); // 📝 REGISTER USER (dengan validasi middleware) app.post('/api/register', validateUserRegistration, async (req, res) => { try { console.log('📥 Data yang akan disimpan (sudah disanitasi):', req.body); // Simpan ke database (async operation) const savedUser = await simulatedDatabase.saveUser(req.body); // Hapus password dari response const { password, ...userWithoutPassword } = savedUser; res.status(201).json( successResponse( 'User berhasil diregistrasi', userWithoutPassword, { warnings: req.validation?.warnings || [], validation: 'PASSED' } ) ); } catch (error) { console.error('🔥 Error saat registrasi:', error); // Handle specific database errors if (error.name === 'DuplicateError') { return res.status(409).json( errorResponse( 409, 'DUPLICATE_ENTRY', error.message, { field: error.field, suggestion: 'Gunakan email lain atau lupa password?' } ) ); } // Handle database connection errors if (error.message === 'Database connection failed') { return res.status(503).json( errorResponse( 503, 'SERVICE_UNAVAILABLE', 'Database sedang mengalami masalah', { retryAfter: 30, // detik suggestion: 'Coba lagi dalam 30 detik' } ) ); } // Generic error res.status(500).json( errorResponse( 500, 'INTERNAL_SERVER_ERROR', 'Terjadi kesalahan saat registrasi', process.env.NODE_ENV === 'development' ? { stack: error.stack } : null ) ); } }); // 🔐 LOGIN USER app.post('/api/login', validateLogin, async (req, res) => { try { const { email, password } = req.body; // Cari user di database const user = await simulatedDatabase.findUserByEmail(email); if (!user) { return res.status(401).json( errorResponse( 401, 'AUTHENTICATION_FAILED', 'Email atau password salah', { hint: 'Periksa kembali email dan password Anda', remainingAttempts: 4 // biasanya tracking di session } ) ); } // Simulasi: cek password (dalam real app, compare hash) if (password !== user.password) { return res.status(401).json( errorResponse( 401, 'AUTHENTICATION_FAILED', 'Email atau password salah', { hint: 'Password yang dimasukkan tidak sesuai', remainingAttempts: 3 } ) ); } // Simulasi: generate token const token = `jwt_${Date.now()}_${Math.random().toString(36).substr(2)}`; res.json( successResponse( 'Login berhasil', { user: { id: user.id, name: user.name, email: user.email }, token: token, expiresIn: '24h' } ) ); } catch (error) { console.error('🔥 Error saat login:', error); res.status(500).json( errorResponse( 500, 'LOGIN_FAILED', 'Terjadi kesalahan saat login', process.env.NODE_ENV === 'development' ? { error: error.message } : null ) ); } }); // 👥 GET ALL USERS (with error simulation) app.get('/api/users', async (req, res) => { try { // Simulasi: authorization check const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { return res.status(401).json( errorResponse( 401, 'UNAUTHORIZED', 'Token autentikasi diperlukan', { requiredHeader: 'Authorization: Bearer <token>', example: 'Authorization: Bearer jwt_token_123' } ) ); } // Simulasi: admin check const isAdmin = authHeader.includes('admin'); if (!isAdmin) { return res.status(403).json( errorResponse( 403, 'FORBIDDEN', 'Hanya admin yang bisa mengakses data users', { requiredRole: 'admin', currentRole: 'user' } ) ); } // Return users (without passwords) const usersWithoutPasswords = users.map(({ password, ...user }) => user); res.json( successResponse( 'Users retrieved successfully', usersWithoutPasswords, { count: users.length, role: 'admin', filtered: 'passwords hidden' } ) ); } catch (error) { console.error('🔥 Error saat mengambil users:', error); res.status(500).json( errorResponse( 500, 'FETCH_USERS_ERROR', 'Gagal mengambil data users' ) ); } }); // ==================== ERROR DEMONSTRATION ROUTES ==================== // 🎭 DEMO BERBAGAI JENIS ERROR app.get('/api/error/demo/:type', (req, res) => { const errorType = req.params.type; switch(errorType) { case 'validation': // Simulasi validation error return res.status(400).json( errorResponse( 400, 'VALIDATION_ERROR', 'Data yang dikirim tidak valid', { errors: [ { field: 'email', message: 'Format email tidak valid', code: 'INVALID_EMAIL' }, { field: 'password', message: 'Password terlalu pendek', code: 'PASSWORD_TOO_SHORT', minLength: 8, currentLength: 5 } ], totalErrors: 2 } ) ); case 'not-found': // Simulasi resource not found return res.status(404).json( errorResponse( 404, 'NOT_FOUND', 'Resource tidak ditemukan', { resource: 'User', id: req.query.id || 'unknown', suggestion: 'Periksa ID atau coba resource lain' } ) ); case 'unauthorized': // Simulasi unauthorized access return res.status(401).json( errorResponse( 401, 'UNAUTHORIZED', 'Anda harus login untuk mengakses resource ini', { required: 'Valid authentication token', solution: 'Login terlebih dahulu' } ) ); case 'forbidden': // Simulasi forbidden access return res.status(403).json( errorResponse( 403, 'FORBIDDEN', 'Anda tidak memiliki izin untuk mengakses resource ini', { requiredRole: 'admin', currentRole: 'user', suggestion: 'Hubungi administrator' } ) ); case 'rate-limit': // Simulasi rate limit return res.status(429).json( errorResponse( 429, 'RATE_LIMIT_EXCEEDED', 'Terlalu banyak request', { limit: '100 requests per hour', resetIn: '15 minutes', suggestion: 'Coba lagi nanti' } ) ); case 'server-error': // Simulasi server error (unhandled exception) throw new Error('Simulated server error: Something went wrong!'); case 'timeout': // Simulasi timeout setTimeout(() => { res.json(successResponse('Response setelah timeout')); }, 5000); // 5 seconds delay return; default: return res.status(400).json( errorResponse( 400, 'INVALID_ERROR_TYPE', 'Tipe error demo tidak valid', { validTypes: [ 'validation', 'not-found', 'unauthorized', 'forbidden', 'rate-limit', 'server-error', 'timeout' ], received: errorType } ) ); } }); // ==================== CUSTOM ERROR CLASSES ==================== // Custom Error Classes untuk error handling yang lebih baik class AppError extends Error { constructor(message, statusCode, errorCode, details = null) { super(message); this.statusCode = statusCode; this.errorCode = errorCode; this.details = details; this.isOperational = true; // Error yang diharapkan (bukan bug) Error.captureStackTrace(this, this.constructor); } } class ValidationError extends AppError { constructor(message, errors) { super(message, 400, 'VALIDATION_ERROR', { errors }); this.name = 'ValidationError'; } } class NotFoundError extends AppError { constructor(resource, id) { super(`${resource} dengan ID ${id} tidak ditemukan`, 404, 'NOT_FOUND', { resource, id }); this.name = 'NotFoundError'; } } class AuthenticationError extends AppError { constructor(message = 'Autentikasi gagal') { super(message, 401, 'AUTHENTICATION_FAILED'); this.name = 'AuthenticationError'; } } // ==================== ROUTES DENGAN CUSTOM ERROR ==================== // 📚 CREATE BOOK dengan custom error app.post('/api/books', async (req, res, next) => { try { const { title, author, isbn } = req.body; // Validasi menggunakan custom error if (!title || !author || !isbn) { throw new ValidationError('Data buku tidak lengkap', [ { field: 'title', required: true }, { field: 'author', required: true }, { field: 'isbn', required: true } ]); } // Simulasi: cek ISBN unik const isbnExists = false; // Simulasi cek database if (isbnExists) { throw new AppError( 'ISBN sudah terdaftar', 409, 'DUPLICATE_ISBN', { isbn, suggestion: 'Gunakan ISBN lain' } ); } // Simulasi success const newBook = { id: Date.now(), title, author, isbn, createdAt: new Date().toISOString() }; res.status(201).json( successResponse('Buku berhasil dibuat', newBook) ); } catch (error) { next(error); // Pass error ke error handling middleware } }); // 🔍 GET BOOK BY ID dengan custom error app.get('/api/books/:id', async (req, res, next) => { try { const bookId = req.params.id; if (!bookId || isNaN(bookId)) { throw new ValidationError('ID buku tidak valid', [ { field: 'id', message: 'ID harus berupa angka' } ]); } // Simulasi: cari buku (tidak ditemukan) const book = null; // Simulasi tidak ditemukan if (!book) { throw new NotFoundError('Buku', bookId); } res.json( successResponse('Buku ditemukan', book) ); } catch (error) { next(error); } }); // ==================== GLOBAL ERROR HANDLING MIDDLEWARE ==================== // 1. 404 Handler (harus di akhir routes, sebelum error handler) app.use('*', (req, res, next) => { const error = new AppError( `Route ${req.method} ${req.originalUrl} tidak ditemukan`, 404, 'ROUTE_NOT_FOUND' ); next(error); }); // 2. Global Error Handler (harus di paling akhir) app.use((error, req, res, next) => { console.error('🔥 GLOBAL ERROR HANDLER:', error); // Default values const statusCode = error.statusCode || 500; const errorCode = error.errorCode || 'INTERNAL_SERVER_ERROR'; const message = error.message || 'Terjadi kesalahan pada server'; // Development vs Production error details const errorDetails = { type: error.name || 'Error', code: errorCode, message: message, timestamp: new Date().toISOString(), path: req.originalUrl, method: req.method }; // Tambahkan stack trace hanya di development if (process.env.NODE_ENV === 'development') { errorDetails.stack = error.stack; errorDetails.error = error; } // Tambahkan validation errors jika ada if (error.details) { errorDetails.details = error.details; } // Log error (dalam real app, log ke file/service) logErrorToFile(error, req); // Send response res.status(statusCode).json({ success: false, error: errorDetails }); }); // Simulasi error logging function logErrorToFile(error, req) { const logEntry = { timestamp: new Date().toISOString(), url: req.originalUrl, method: req.method, error: { name: error.name, message: error.message, stack: error.stack }, userAgent: req.headers['user-agent'], ip: req.ip || req.connection.remoteAddress }; console.log('📝 ERROR LOG:', JSON.stringify(logEntry, null, 2)); } // ==================== ASYNC ERROR HANDLING WRAPPER ==================== // Utility untuk wrap async functions dengan error handling const asyncHandler = (fn) => { return (req, res, next) => { Promise.resolve(fn(req, res, next)).catch(next); }; }; // Contoh penggunaan asyncHandler app.get('/api/async-demo', asyncHandler(async (req, res) => { // Async operation yang bisa throw error const data = await someAsyncOperation(); if (!data) { throw new AppError('Data tidak ditemukan', 404, 'DATA_NOT_FOUND'); } res.json(successResponse('Success', data)); })); function someAsyncOperation() { return new Promise((resolve, reject) => { // Simulasi async operation setTimeout(() => { if (Math.random() > 0.5) { resolve({ id: 1, name: 'Test Data' }); } else { reject(new Error('Random async error')); } }, 100); }); } // ==================== SECURITY MIDDLEWARE ==================== // Basic security middleware app.use((req, res, next) => { // Coba parse JSON, jika gagal, tangani error if (req.is('application/json') && req.body) { try { // Body sudah diparse oleh express.json() // Tambahkan validasi tambahan jika perlu next(); } catch (error) { res.status(400).json( errorResponse( 400, 'INVALID_JSON', 'Request body mengandung JSON yang tidak valid', process.env.NODE_ENV === 'development' ? { error: error.message } : null ) ); } } else { next(); } }); // ==================== RATE LIMITING SIMULATION ==================== const requestCounts = new Map(); app.use((req, res, next) => { const ip = req.ip || req.connection.remoteAddress; const now = Date.now(); const windowMs = 60 * 1000; // 1 minute window const maxRequests = 10; // Max 10 requests per minute // Clean old entries requestCounts.forEach((timestamps, key) => { const validTimestamps = timestamps.filter(time => now - time < windowMs); if (validTimestamps.length === 0) { requestCounts.delete(key); } else { requestCounts.set(key, validTimestamps); } }); // Check current IP const timestamps = requestCounts.get(ip) || []; if (timestamps.length >= maxRequests) { return res.status(429).json( errorResponse( 429, 'RATE_LIMIT_EXCEEDED', 'Terlalu banyak request', { limit: `${maxRequests} requests per minute`, resetIn: Math.ceil((timestamps[0] + windowMs - now) / 1000), suggestion: 'Coba lagi nanti' } ) ); } // Add current timestamp timestamps.push(now); requestCounts.set(ip, timestamps); // Add rate limit info to response headers res.setHeader('X-RateLimit-Limit', maxRequests); res.setHeader('X-RateLimit-Remaining', maxRequests - timestamps.length); next(); }); // ==================== START SERVER ==================== app.listen(PORT, () => { console.log("=".repeat(70)); console.log("🛡️ VALIDATION & ERROR HANDLING PRACTICE SERVER"); console.log("=".repeat(70)); console.log(`🌐 Server: http://localhost:${PORT}`); console.log(""); console.log("🎯 COBA ENDPOINT BERIKUT:"); console.log(""); console.log("1. Validasi Input:"); console.log(" POST /api/register"); console.log(' Body: {"name": "a", "email": "invalid", "password": "123"}'); console.log(""); console.log("2. Error Handling:"); console.log(" POST /api/register"); console.log(' Body: {"name": "Test", "email": "test@test.com", "password": "StrongPass123!"}'); console.log(" → Coba beberapa kali untuk lihat database error simulasi"); console.log(""); console.log("3. Authentication Error:"); console.log(" GET /api/users"); console.log(" → Tanpa authorization header"); console.log(""); console.log("4. Demo Berbagai Error:"); console.log(" GET /api/error/demo/validation"); console.log(" GET /api/error/demo/not-found"); console.log(" GET /api/error/demo/unauthorized"); console.log(" GET /api/error/demo/server-error"); console.log(""); console.log("5. Custom Error Classes:"); console.log(" POST /api/books"); console.log(' Body: {"title": ""} → ValidationError'); console.log(" GET /api/books/999 → NotFoundError"); console.log(""); console.log("6. Rate Limiting:"); console.log(" Refresh homepage berkali-kali (lebih dari 10x dalam 1 menit)"); console.log("=".repeat(70)); });
3.3 Testing Error Scenarios dengan Postman
Test 1: Validasi Error
POST http://localhost:3005/api/register Content-Type: application/json { "name": "a", "email": "bukan-email", "password": "123" }
Expected Response (400):
{ "success": false, "error": { "type": "VALIDATION_ERROR", "message": "Validasi data gagal", "details": { "errors": [ { "field": "name", "message": "Nama minimal 3 karakter", "code": "MIN_LENGTH", "min": 3, "current": 1 }, { "field": "email", "message": "Format email tidak valid", "code": "INVALID_FORMAT", "example": "user@example.com", "received": "bukan-email" }, { "field": "password", "message": "Password terlalu lemah", "code": "WEAK_PASSWORD", "requirements": [ "minimal 8 karakter", "minimal 1 huruf besar", "minimal 1 karakter khusus" ] } ] } } }
Test 2: Database Error Simulation
POST http://localhost:3005/api/register Content-Type: application/json { "name": "John Doe", "email": "john@example.com", "password": "StrongPass123!" }
Coba beberapa kali - 10% kemungkinan dapat error database:
{ "success": false, "error": { "type": "SERVICE_UNAVAILABLE", "message": "Database sedang mengalami masalah", "details": { "retryAfter": 30, "suggestion": "Coba lagi dalam 30 detik" } } }
Test 3: Authentication Error
GET http://localhost:3005/api/users # Tanpa authorization header
Expected Response (401):
{ "success": false, "error": { "type": "UNAUTHORIZED", "message": "Token autentikasi diperlukan", "details": { "requiredHeader": "Authorization: Bearer <token>", "example": "Authorization: Bearer jwt_token_123" } } }
3.4 Best Practices Validasi dan Error Handling
1. Validasi di Multiple Layers
// ✅ Defense in depth // Layer 1: Client-side validation (UI) // Layer 2: Server-side validation (middleware) // Layer 3: Database constraints (unique, not null) // Layer 4: Business logic validation // Contoh middleware chain app.post('/api/users', validateRequestBody, // Cek body ada/tidak sanitizeInput, // Bersihkan input validateDataTypes, // Cek tipe data validateBusinessRules, // Cek aturan bisnis asyncHandler(createUser) // Handler utama );
2. Gunakan Library Validasi
// Menggunakan Joi (npm install joi) const Joi = require('joi'); const userSchema = Joi.object({ name: Joi.string().min(3).max(100).required(), email: Joi.string().email().required(), age: Joi.number().min(13).max(120).optional(), password: Joi.string() .min(8) .pattern(new RegExp('^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*])')) .required() }); // Dalam middleware const validation = userSchema.validate(req.body); if (validation.error) { return res.status(400).json({ error: validation.error.details }); }
3. Structured Error Responses
// ❌ Bad - inconsistent error format res.status(400).send('Email invalid'); res.status(404).json({ error: 'Not found' }); res.status(500).send('Server error'); // ✅ Good - consistent error format const errorResponse = { success: false, error: { code: 'VALIDATION_ERROR', message: 'Email format is invalid', field: 'email', details: { /* optional */ } }, timestamp: new Date().toISOString(), requestId: req.requestId // untuk tracking };
4. Logging yang Baik
// Log dengan context yang cukup function logError(error, req) { logger.error({ message: error.message, stack: error.stack, url: req.originalUrl, method: req.method, userId: req.user?.id, ip: req.ip, userAgent: req.headers['user-agent'], body: process.env.NODE_ENV === 'development' ? req.body : undefined, timestamp: new Date().toISOString() }); }
5. Graceful Degradation
app.get('/api/data', async (req, res) => { try { const data = await fetchDataFromAPI(); return res.json({ success: true, data }); } catch (error) { // Jika API gagal, coba dari cache const cachedData = await getFromCache(); if (cachedData) { return res.json({ success: true, data: cachedData, warning: 'Data dari cache (API sedang bermasalah)' }); } // Jika cache juga kosong, baru error throw error; } });
3.5 Common Security Vulnerabilities dan Prevention
1. SQL Injection
// ❌ Vulnerable const query = `SELECT * FROM users WHERE email = '${email}'`; // ✅ Safe (gunakan parameterized queries) const query = 'SELECT * FROM users WHERE email = ?'; db.query(query, [email]);
2. XSS (Cross-Site Scripting)
// ❌ Vulnerable res.send(`<div>${userInput}</div>`); // ✅ Safe (sanitize atau escape) res.send(`<div>${escapeHtml(userInput)}</div>`); // Atau gunakan template engine yang auto-escape
3. Mass Assignment
// ❌ Vulnerable (client bisa set role admin) const user = req.body; db.save(user); // Jika body contains {role: 'admin'} // ✅ Safe (whitelist fields) const { name, email, password } = req.body; const user = { name, email, password }; db.save(user);
3.6 Monitoring dan Alerting
// Setup error monitoring const setupErrorMonitoring = () => { process.on('uncaughtException', (error) => { console.error('🔥 Uncaught Exception:', error); // Kirim alert ke Slack/Email sendAlert('CRITICAL: Uncaught Exception', error); // Graceful shutdown process.exit(1); }); process.on('unhandledRejection', (reason, promise) => { console.error('🔥 Unhandled Rejection at:', promise, 'reason:', reason); sendAlert('WARNING: Unhandled Rejection', { reason }); }); };
3.7 Latihan Praktikum
Exercise 1: E-commerce Checkout Validation
// Buat validasi untuk checkout: // 1. Validasi cart items (array of products with quantities) // 2. Validasi shipping address // 3. Validasi payment method // 4. Business rules: stock availability, minimum order, etc. // 5. Custom errors untuk setiap skenario
Exercise 2: File Upload Validation
// Buat validasi untuk file upload: // 1. File size limit (max 5MB) // 2. File type restriction (jpg, png, pdf only) // 3. Virus scanning simulation // 4. Duplicate file detection // 5. Storage quota check
4. Daftar Pustaka
- NusaCodes (2025). Handling Error di Express.js: Contoh Best Practice untuk Aplikasi yang Lebih Stabil. https://nusacodes.com
- Express.js Error Handling Guide (n.d.). Error Handling. https://expressjs.com
- DigitalOcean (2024). How to Handle Form Inputs Efficiently with Express-Validator in ExpressJS. https://www.digitalocean.com
- DevTo (2025). Global Error Handling in Express.js: Best Practices. https://dev.to/shyamtala
- Santekno (2025). 114 Membuat Middleware Validasi Global untuk Input. https://www.santekno.com

0 Komentar