
Middleware Express.js yang Wajib Ada di Setiap Proyek Produksi: Daftar dari Pengalaman Pribadi
Daftar middleware Express.js yang saya pasang di semua aplikasi produksi: rate limiting, error handler, helmet, hpp, hingga logging terstruktur — lengkap dengan contoh kode siap pakai.
Minggu lalu seorang junior di tim saya deploy API Express.js pertamanya ke staging, dan dalam dua jam ada satu endpoint yang dipanggil 40.000 kali oleh crawler nakal. Tidak ada rate limiter, tidak ada timeout, dan error handler default Express membocorkan stack trace ke response. Bukan salah dia — tutorial Express.js kebanyakan berhenti di "hello world" dan tidak pernah menunjukkan middleware apa yang dibutuhkan supaya aplikasi aman dipakai di produksi.
Setelah sepuluh tahun membangun aplikasi Node.js — beberapa di antaranya menangani jutaan request per hari — saya punya daftar middleware yang saya pasang hampir tanpa kecuali di setiap proyek produksi. Ini bukan daftar teoretis; setiap item di bawah ada cerita insiden di baliknya.
Urutan Middleware Itu Penting, Bukan Hanya Isinya
Kesalahan pertama yang sering saya lihat: developer memasang semua middleware yang "benar" tapi urutannya kacau. Middleware Express dieksekusi berurutan dari atas ke bawah, jadi helmet harus sebelum route, rate limiter idealnya sesegera mungkin, dan error handler harus paling akhir setelah semua route.
Kerangka dasar yang saya pakai di hampir semua proyek:
const express = require('express');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const cors = require('cors');
const app = express();
app.set('trust proxy', 1); // penting kalau di belakang reverse proxy
app.disable('x-powered-by');
app.use(helmet());
app.use(express.json({ limit: '1mb' }));
app.use(express.urlencoded({ extended: true, limit: '1mb' }));
app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(',') }));
app.use(globalLimiter);
// routes di sini...
app.use(notFoundHandler);
app.use(errorHandler); // selalu paling akhir
Catatan penting: app.set('trust proxy', 1) sering dilupakan padahal krusial. Tanpa itu, express-rate-limit akan melihat IP reverse proxy (misalnya IP load balancer) sebagai IP client — dan semua user akan share satu bucket rate limit yang sama. Saya pernah kena ini di produksi: semua pengguna mendapat 429 secara bergiliran setelah traffic naik.
1. Helmet: Header Keamanan dalam Satu Baris
Helmet mengatur serangkaian header keamanan HTTP: Strict-Transport-Security, X-Content-Type-Options, X-Frame-Options, Content-Security-Policy, dan lainnya. Untuk API murni sebagian header tidak terlalu berpengaruh, tapi untuk aplikasi yang menyajikan HTML, CSP bisa mencegah XSS berdampak besar.
Satu jebakan yang saya alami: default helmet memasang Content-Security-Policy yang ketat, dan tiba-tiba frontend yang memuat script dari CDN patah semua. Solusinya bukan mematikan helmet, tapi menyesuaikan:
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "https://cdn.example.com"],
},
},
}));
Untuk API yang tidak menyajikan HTML, saya biasanya matikan CSP saja: helmet({ contentSecurityPolicy: false }).

2. Rate Limiting: Saya Pasang Dua Lapis
Insiden crawler 40.000 request yang saya sebut di pembuka bisa dicegah dengan satu middleware. Setup yang saya pakai adalah dua lapis:
const globalLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
limit: 300, // 300 request per 15 menit per IP
standardHeaders: 'draft-7',
legacyHeaders: false,
});
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
limit: 10, // endpoint login jauh lebih ketat
skipSuccessfulRequests: true,
});
app.use('/api/login', loginLimiter);
Endpoint login dan reset password selalu dapat limiter terpisah yang lebih ketat — ini juga mitigasi brute force sederhana. Kalau aplikasi Anda multi-tenant SaaS, pertimbangkan limiter per-tenant (key berdasarkan API key atau tenant ID, bukan IP) supaya satu tenant bermasalah tidak melumpuhkan yang lain.
Trade-off yang jujur: rate limit berbasis memori default tidak berfungsi konsisten kalau Anda jalan di beberapa instance. Untuk skala itu, gunakan rate-limit-redis sebagai store bersama — dan ini nyambung baik dengan arsitektur Redis sebagai cache layer di Node.js yang sudah saya tulis sebelumnya.
3. Body Parser dengan Limit yang Eksplisit
express.json() tanpa limit berarti siapa pun bisa mengirim body 500 MB dan Node.js akan mencoba mem-parsing semuanya — ini pintu masuk serangan DoS klasik. Selalu set limit eksplisit:
app.use(express.json({ limit: '1mb' }));
Angka 1mb cukup untuk hampir semua API JSON. Endpoint upload file sebaiknya tidak lewat body parser JSON sama sekali — pisahkan ke route dengan multer yang punya limit sendiri.
4. CORS yang Dikonfigurasi, Bukan Default
app.use(cors()) tanpa opsi sama dengan mengizinkan semua origin — masih saya lihat di codebase produksi lebih sering dari yang saya mau akui. Minimal:
app.use(cors({
origin: ['https://app.example.com', 'https://admin.example.com'],
credentials: true,
}));
Kalau Anda sedang merancang API dari nol, keputusan seperti ini sebaiknya sudah dipikirkan di fase desain, bukan ditempel belakangan. Prinsip-prinsipnya saya rangkum di tulisan tentang best practice REST API design untuk skala produksi.
5. Error Handler Custom: Jangan Pernah Percaya Default Express
Error handler default Express mengembalikan stack trace ke client. Di staging itu "memudahkan debugging"; di produksi itu bocor informasi infrastruktur. Handler custom yang saya pakai kurang lebih begini:
function errorHandler(err, req, res, next) {
const status = err.status || err.statusCode || 500;
const isProd = process.env.NODE_ENV === 'production';
req.log?.error({ err, path: req.path }, 'unhandled error');
res.status(status).json({
error: {
message: status >= 500 && isProd ? 'Internal server error' : err.message,
...(err.code ? { code: err.code } : {}),
},
});
}
Perhatikan: di produksi, error 5xx selalu mengembalikan pesan generik. Detail aslinya masuk log, bukan ke response. Pola content negotiation — merespon JSON untuk client API dan HTML untuk browser — juga bisa ditangani dalam handler seperti ini; pertanyaan lama di Stack Overflow tentang cara menangani request JSON dan HTML dalam satu handler Express membahas pendekatan yang masih relevan sampai sekarang.
6. Logging Terstruktur dengan Request ID
console.log bukan logging. Di produksi saya pakai pino plus middleware yang menyuntik request ID, sehingga setiap log line bisa ditelusuri per request:
const { randomUUID } = require('crypto');
app.use((req, res, next) => {
req.id = req.headers['x-request-id'] || randomUUID();
req.log = logger.child({ requestId: req.id, path: req.path });
res.setHeader('x-request-id', req.id);
const start = Date.now();
res.on('finish', () => {
req.log.info({ status: res.statusCode, durationMs: Date.now() - start });
});
next();
});
Middleware ini tempat terbaik untuk menaruh log akses karena res.on('finish') menangkap status code final. Ketika terjadi insiden, request ID inilah yang membuat Anda bisa mencari satu request bermasalah di antara jutaan baris log dalam hitungan detik.

7. Timeout dan Graceful Shutdown
Node.js secara default tidak punya timeout per request di level aplikasi. Kalau satu downstream lambat, koneksi menumpuk. Middleware sederhana dengan AbortSignal di handler yang memanggil service eksternal sudah banyak membantu saya:
const fetchWithTimeout = (url, opts = {}, ms = 5000) =>
fetch(url, { ...opts, signal: AbortSignal.timeout(ms) });
Nilai 5000ms adalah angka yang saya pakai untuk panggilan API pihak ketiga — cukup longgar untuk koneksi normal, cukup ketat untuk mencegah penumpukan koneksi. Ditambah server.close() saat menerima SIGTERM, ini kombinasi yang membuat deploy bergulir tidak memutus request yang sedang berjalan.
Yang Tidak Saya Pasang (dan Kenapa)
Supaya tidak terkesan semua middleware wajib ada: saya tidak memakai morgan di produksi (pino + middleware custom lebih terstruktur), tidak memakai hpp di API yang tidak pernah membaca query array, dan tidak memakai cookie-session untuk aplikasi besar — topik JWT vs session untuk autentikasi produksi layak dibahas terpisah karena trade-off-nya bergantung arsitektur.
Menambah middleware "karena kelihatannya berguna" juga punya biaya: setiap layer menambah latency kecil dan satu titik yang bisa gagal. Evaluasi per proyek, bukan copy-paste buta.
Takeaway: Checklist yang Bisa Anda Pakai Hari Ini
Kalau Anda hanya punya waktu satu jam untuk memperbaiki aplikasi Express yang sudah berjalan, urutkan seperti ini: (1) pasang helmet(), (2) set limit di express.json(), (3) pasang rate limiter di endpoint login, (4) ganti error handler default dengan versi custom yang tidak membocorkan stack trace, (5) tambahkan request ID di logging. Lima langkah itu sudah menghilangkan 90% masalah produksi yang pernah saya tangani. Selebihnya — timeout, graceful shutdown, limiter terdistribusi — tambahkan saat skalanya menuntut.
Sumber
- Stack Overflow: capture request to both JSON and HTML in one Express.js function — pola content negotiation di error handler Express yang saya rujuk di section error handling.
- Stack Overflow: What does Express.js do in the MEAN stack? — konteks peran Express sebagai layer routing/middleware di stack Node.js.
- Stack Overflow: Use a route as an alias for another route in Express.js — pola
app.use()untuk menyusun rantai middleware dan mount route yang relevan dengan urutan middleware.
Tomi Hartanto
Senior Software Engineer
Backend developer dengan 8+ tahun pengalaman membangun sistem scalable. Menulis tentang arsitektur backend, database, dan DevOps.
Artikel Terkait

N+1 Query Problem di Prisma dan Laravel Eloquent: Studi Kasus dan Cara Saya Memperbaikinya

Struktur Folder Proyek Backend yang Scalable: Peluang dari Sistem Enterprise yang Saya Kerjakan

Background Job dan Queue untuk Task Berat: Konsep dan Implementasi dengan BullMQ
Tomi Hartanto
Senior Software EngineerBackend developer dengan 8+ tahun pengalaman membangun sistem scalable. Menulis tentang arsitektur backend, database, dan DevOps.