Middleware / Chain of Responsibility — Design Patterns

Chain of Responsibility membuat rantai handler yang memproses request satu per satu. Setiap handler bisa memproses, memodifikasi, atau menolak request sebelum…

Middleware adalah function kecil yang berdiri di jalur sebuah request, sebelum request itu sampai ke handler yang mengerjakannya. Tiap middleware memeriksa satu hal, lalu memilih: meneruskan request ke middleware berikutnya dengan memanggil next(), atau menghentikannya dengan langsung mengirim response. Rantai pemeriksa seperti ini adalah pattern Chain of Responsibility: request diproses oleh rantai handler, satu per satu, dan tiap handler boleh menambah data, menolak, atau meneruskan. Bayangin masuk ke ruang tunggu bandara. Petugas pertama cek tiket, petugas kedua cek KTP, lalu X-ray. Lolos, kamu dipersilakan ke petugas berikutnya. Gagal, kamu dipulangkan di situ, dan petugas sesudahnya gak pernah melihatmu.

🧪 Tebakan cepat: middleware rateLimit di API Kurir Kancil menghitung request, tapi lupa memanggil next() dan gak mengirim response apa pun. Aplikasi kurir mengirim request buat kiriman. Apa yang diterimanya: error 500, response kosong, atau yang lain? Tebak dulu, jawabannya ada di bagian "Lupa next(): request menggantung".

Masalahnya: satu handler mengurus semuanya

Versi pertama endpoint buat kiriman memeriksa semuanya sendiri:

function buatKiriman(req, res) {
  const user = TOKENS[req.token]
  if (!user) return res.send(401, "Unauthorized")
  hits[user] = (hits[user] || 0) + 1
  if (hits[user] > 3) return res.send(429, "Too Many Requests")
  if (!(req.body.kg > 0)) return res.send(422, "kg harus lebih dari 0")
  // ...baru di sini kiriman dibuat
}

Endpoint lacak, batalkan, dan ubah alamat butuh cek token dan batas request yang sama. Tiga pemeriksaan di empat endpoint berarti 12 salinan. Begitu satu endpoint lupa menyalin cek token, endpoint itu terbuka untuk siapa saja, tanpa error yang memberi tahu.

Middleware: satu pemeriksaan, lalu next()

Pecah tiap pemeriksaan jadi function sendiri dengan bentuk yang sama, (req, res, next), lalu susun jadi rantai:

const TOKENS = { "tok-sari": "sari", "tok-budi": "budi", "tok-rudi": "rudi", "tok-dewi": "dewi" }
const hits = {} // jumlah request per user
let nomor = 0

function auth(req, res, next) {
  const user = TOKENS[req.token]
  if (!user) return res.send(401, "Unauthorized") // berhenti di sini
  req.user = user // dibaca middleware sesudahnya
  next()
}

function rateLimit(req, res, next) {
  hits[req.user] = (hits[req.user] || 0) + 1
  if (hits[req.user] > 3) return res.send(429, "Too Many Requests")
  next()
}

function validate(req, res, next) {
  if (!(req.body.kg > 0)) return res.send(422, "kg harus lebih dari 0")
  next()
}

function buatKiriman(req, res) {
  nomor = nomor + 1
  res.send(201, "KCL-" + String(nomor).padStart(4, "0") + " untuk " + req.user)
}

function handle(chain, req) {
  const res = { status: null, send(status, body) { this.status = status; this.body = body } }
  let i = 0
  function next() {
    const mw = chain[i] // middleware berikutnya di rantai
    i = i + 1
    mw(req, res, next)
  }
  next()
  return res.status ? res.status + " " + res.body : "(menggantung, belum ada response)"
}

const chain = [auth, rateLimit, validate, buatKiriman]
console.log(handle(chain, { token: "tok-sari", body: { kg: 2 } })) // 201 KCL-0001 untuk sari
console.log(handle(chain, { token: "", body: { kg: 1 } }))         // 401 Unauthorized
console.log(handle(chain, { token: "tok-budi", body: { kg: 0 } })) // 422 kg harus lebih dari 0
request auth rateLimit validate buatKiriman response
sari, 2 kg lolos lolos (1) lolos jalan 201 KCL-0001 untuk sari
tanpa token tolak gak sampai gak sampai gak sampai 401 Unauthorized
budi, 0 kg lolos lolos (1) tolak gak sampai 422 kg harus lebih dari 0

Endpoint lain tinggal memakai rantai yang sama dengan handler-nya sendiri, misalnya [auth, rateLimit, lacak]. Cek token ditulis sekali.

Lupa next(): request menggantung

Hapus next() dari rateLimit. Request sari berhenti di situ: rateLimit gak menolak, gak juga meneruskan, dan gak ada yang mengirim response. handle mengembalikan (menggantung, belum ada response).

Itu jawaban tebakannya: bukan error 500, bukan response kosong, tapi gak ada response sama sekali. Express gak punya batas waktu untuk ini. Aplikasi kurir menunggu sampai dia sendiri menyerah, atau sampai proxy di depan server memutusnya (nginx memutus sesudah 60 detik dengan status 504). Log server bersih, karena dari sisi JavaScript gak ada yang error.

Kebalikannya sama berbahayanya, lupa return sesudah menolak: if (!user) res.send(401, "Unauthorized"). Response 401 terkirim, tapi function terus jalan. next() tetap dipanggil, request tanpa token sampai ke buatKiriman, dan kiriman dibuat untuk undefined. Saat handler mencoba mengirim response kedua, Express melempar Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client, sesudah kirimannya tersimpan.

Urutan menentukan siapa yang ditolak

rateLimit menghitung per user, jadi dia butuh req.user dari auth. Kirim lima request dengan dua urutan rantai:

request [auth, rateLimit, ...] [rateLimit, auth, ...]
#1 sari, 2 kg 201 KCL-0001 201 KCL-0001
#2 tanpa token 401 401
#3 budi, 0 kg 422 422
#4 rudi, 1 kg 201 KCL-0002 429
#5 dewi, 3 kg 201 KCL-0003 429

Di urutan kedua, rateLimit jalan saat req.user masih undefined, jadi kelima request dihitung di satu kunci yang sama, hits["undefined"]. Request rudi adalah hitungan ke-4, dan dia ditolak 429 Too Many Requests di request pertamanya. Gak ada error: kodenya jalan, cuma orang yang salah yang ditolak.

Pemeriksaan yang butuh data dari pemeriksaan lain harus ditaruh sesudahnya. Batas request per IP, yang gak butuh req.user, malah biasanya ditaruh paling depan, supaya tebakan password berulang ditolak sebelum sampai ke auth.

Middleware di Express, Koa, dan browser

Express memakai bentuk yang sama persis, (req, res, next). app.use(fn) memasang middleware untuk semua route, dan app.post("/kiriman", auth, rateLimit, validate, buatKiriman) memasang rantai untuk satu route. Menolak ditulis return res.status(401).json({ error: "Unauthorized" }). Middleware Laravel sama: menolak, atau meneruskan dengan return $next($request). Koa memakai async (ctx, next) => { ... await next() ... }: kode sesudah await next() jalan saat rantai kembali dari handler, jadi satu middleware bisa mencatat waktu, menunggu next(), lalu menghitung lama request-nya.

Browser punya rantai serupa: event klik naik dari tombol ke elemen induknya sampai document, dan listener mana pun di jalur itu bisa menghentikannya dengan event.stopPropagation().

Rantai juga bisa dirangkai dengan tangan, dengan next yang menerima req:

function m1(req, next) { req.user = "Andi"; next(req) }
function m2(req, next) { req.role = "admin"; next(req) }
function handler(req) { console.log(req.user, req.role) }

m1({}, (r) => m2(r, handler)) // Andi admin

m1 mengisi user lalu meneruskan ke m2, m2 mengisi role lalu memanggil handler. console.log dengan dua argumen mencetaknya dipisah spasi: Andi admin.

Kapan gak perlu middleware

⚠️ Jebakan yang sering ketemu

TL;DR: Middleware adalah function (req, res, next) di jalur request yang memeriksa satu hal, lalu meneruskan dengan next() atau berhenti dengan return res.send(...). Request diproses rantai handler satu per satu (pattern Chain of Responsibility), dengan req yang sama sepanjang rantai. Lupa next() membuat request menggantung tanpa response, lupa return membuat rantai jalan terus sesudah menolak. Urutan menentukan siapa yang ditolak: rateLimit per user harus sesudah auth, kalau gak, rudi kena 429 di request pertamanya. Express menulis app.post("/kiriman", auth, rateLimit, validate, buatKiriman), Koa memakai await next(), dan browser punya event bubbling dengan stopPropagation().

Yang akan kamu pelajari