API yang baik bukan hanya yang bisa menangani kasus sukses — tapi juga yang memberikan respons error yang informatif, konsisten, dan aman. Di artikel ini kita bahas cara membangun sistem error handling terpusat dan validasi input yang profesional di Express.js.
Masalah dengan Error Handling yang Tidak Konsisten
Tanpa sistem error handling yang terpusat, setiap route menulis error response sendiri-sendiri:
// Tidak konsisten — setiap route format errornya berbeda
app.get('/a', (req, res) => {
res.status(404).send('Tidak ditemukan');
});
app.get('/b', (req, res) => {
res.status(404).json({ message: 'Not found' });
});
app.get('/c', (req, res) => {
res.status(404).json({ error: true, detail: 'Resource missing' });
});
Klien API kamu harus menangani tiga format berbeda. Ini menyulitkan integrasi.
Custom Error Class
Buat class Error kustom yang membawa HTTP status code:
// utils/AppError.js
class AppError extends Error {
constructor(message, statusCode = 500) {
super(message);
this.statusCode = statusCode;
this.isOperational = true; // error yang kita lempar sengaja
Error.captureStackTrace(this, this.constructor);
}
}
module.exports = AppError;
Error Handler Terpusat
// middleware/errorHandler.js
function errorHandler(err, req, res, next) {
const isDev = process.env.NODE_ENV !== 'production';
// Default status dan message
let status = err.statusCode || 500;
let message = err.message || 'Terjadi kesalahan pada server';
// Prisma error - record not found
if (err.code === 'P2025') {
status = 404;
message = 'Data tidak ditemukan';
}
// Prisma error - unique constraint
if (err.code === 'P2002') {
status = 409;
message = `Field '${err.meta?.target}' sudah digunakan`;
}
// JWT error
if (err.name === 'JsonWebTokenError') {
status = 403;
message = 'Token tidak valid';
}
if (err.name === 'TokenExpiredError') {
status = 401;
message = 'Token sudah kadaluarsa, silakan login kembali';
}
// SyntaxError pada JSON body yang tidak valid
if (err instanceof SyntaxError && err.status === 400) {
status = 400;
message = 'Format JSON tidak valid';
}
const response = { error: message };
if (isDev) response.stack = err.stack;
res.status(status).json(response);
}
module.exports = errorHandler;
Daftarkan di index.js — paling akhir setelah semua route:
const errorHandler = require('./middleware/errorHandler');
// ... semua route ...
app.use(errorHandler);
Menggunakan AppError di Route
const AppError = require('../utils/AppError');
app.get('/pengguna/:id', async (req, res, next) => {
try {
const id = parseInt(req.params.id);
if (isNaN(id)) throw new AppError('ID harus berupa angka', 400);
const pengguna = await db.findById(id);
if (!pengguna) throw new AppError('Pengguna tidak ditemukan', 404);
res.json(pengguna);
} catch (err) {
next(err); // teruskan ke error handler terpusat
}
});
Validasi Input dengan express-validator
Library express-validator menyediakan validasi yang deklaratif dan mudah dibaca.
npm install express-validator
Contoh Validasi Registrasi
const { body, validationResult } = require('express-validator');
const aturanValidasiDaftar = [
body('nama')
.trim()
.notEmpty().withMessage('Nama wajib diisi')
.isLength({ min: 2, max: 100 }).withMessage('Nama harus 2-100 karakter'),
body('email')
.trim()
.notEmpty().withMessage('Email wajib diisi')
.isEmail().withMessage('Format email tidak valid')
.normalizeEmail(),
body('password')
.notEmpty().withMessage('Password wajib diisi')
.isLength({ min: 8 }).withMessage('Password minimal 8 karakter')
.matches(/[A-Z]/).withMessage('Password harus mengandung huruf kapital')
.matches(/[0-9]/).withMessage('Password harus mengandung angka'),
body('konfirmasiPassword')
.custom((value, { req }) => {
if (value !== req.body.password) {
throw new Error('Konfirmasi password tidak cocok');
}
return true;
})
];
// Middleware untuk cek hasil validasi
function cekValidasi(req, res, next) {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(422).json({
error: 'Input tidak valid',
detail: errors.array().map(e => ({ field: e.path, pesan: e.msg }))
});
}
next();
}
// Pakai di route
router.post('/register', aturanValidasiDaftar, cekValidasi, async (req, res) => {
// input sudah tervalidasi dan di-sanitize
const { nama, email, password } = req.body;
// ... proses registrasi
});
Validasi Artikel
const aturanArtikel = [
body('judul').trim().notEmpty().withMessage('Judul wajib diisi')
.isLength({ max: 200 }).withMessage('Judul maksimal 200 karakter'),
body('konten').trim().notEmpty().withMessage('Konten wajib diisi')
.isLength({ min: 50 }).withMessage('Konten minimal 50 karakter'),
body('kategori').optional()
.isIn(['tutorial', 'opini', 'berita', 'review'])
.withMessage('Kategori tidak valid'),
body('tags').optional().isArray().withMessage('Tags harus berupa array')
];
router.post('/artikel', authMiddleware, aturanArtikel, cekValidasi, async (req, res) => {
const { judul, konten, kategori } = req.body;
// ...
});
404 Handler untuk Route Tidak Dikenal
// Letakkan setelah semua route, sebelum error handler
app.use((req, res, next) => {
next(new AppError(`Endpoint ${req.method} ${req.url} tidak ditemukan`, 404));
});
// Error handler terakhir
app.use(errorHandler);
Format Response Error yang Konsisten
Dengan pendekatan ini, semua error response memiliki format yang sama:
{
"error": "Pengguna tidak ditemukan"
}
Untuk error validasi:
{
"error": "Input tidak valid",
"detail": [
{ "field": "email", "pesan": "Format email tidak valid" },
{ "field": "password", "pesan": "Password minimal 8 karakter" }
]
}
Klien API kamu hanya perlu menangani satu format — jauh lebih mudah diintegrasikan.
Kesimpulan
Error handling dan validasi yang baik adalah tanda API yang matang. Dengan AppError, error handler terpusat, dan express-validator, semua error ditangani secara konsisten di satu tempat — route handler hanya perlu fokus pada logika bisnis, bukan mengurus format error.
Di artikel berikutnya, kita bahas upload file di Express.js menggunakan Multer.
Kiki/🎮🍉⌨️🍩💻