Kenapa ini penting
Begitu API-mu punya lebih dari selusin rute, menaruh semuanya di satu file server.js akan membuat file itu ribuan baris. Selain itu, ada pekerjaan yang harus dilakukan untuk banyak rute sekaligus: mencatat log, memeriksa login, membaca body, menyajikan file statis. Router memecah rute menjadi file-file kecil per fitur, dan middleware mengurus pekerjaan bersama itu tanpa copy-paste.
Middleware adalah konsep paling penting di Express. Hampir semua fitur Express, termasuk express.json() dan express.static(), adalah middleware.
Konsep inti
Middleware = pos pemeriksaan berurutan
Bayangkan request seperti paket yang lewat ban berjalan di gudang. Di sepanjang ban ada beberapa petugas berurutan: petugas pertama mencatat paket (logger), petugas kedua membuka bungkus (parser JSON), petugas ketiga memeriksa izin (auth), dan petugas terakhir memproses isinya (handler rute). Setiap petugas bisa:
- Melakukan sesuatu pada
req/res(menambahreq.user, misalnya), - Meneruskan paket ke petugas berikutnya dengan memanggil
next(), atau - Menghentikan paket dan langsung menjawab (
res.status(401).json(...)).
Kalau petugas tidak memanggil next() dan juga tidak menjawab, paket berhenti di tengah dan request menggantung.
Bentuk middleware: function (req, res, next) { ... }. Middleware dipasang dengan app.use(fn) (berlaku untuk semua rute di bawahnya), app.use("/api", fn) (hanya path berawalan /api), atau disisipkan di rute tertentu: app.post("/x", wajibLogin, handler).
Urutan itu penting
Express menjalankan middleware dan rute sesuai urutan pendaftaran. Akibatnya:
express.json()harus dipasang sebelum rute yang membacareq.body.- Logger dipasang paling atas supaya semua request tercatat.
- Handler 404 dipasang setelah semua rute: kalau sampai di situ, berarti tidak ada rute yang cocok.
- Error handler (4 parameter) dipasang paling akhir.
express.Router
Router() membuat "mini-aplikasi" yang punya rute dan middleware sendiri, lalu dipasang di sebuah prefix. File routes/produk.js cukup menulis router.get("/") dan router.get("/:id"); saat dipasang dengan app.use("/api/produk", router), keduanya otomatis menjadi /api/produk dan /api/produk/:id. Di dalam router, req.originalUrl tetap berisi URL lengkap, sedangkan req.url dan req.path sudah dipotong prefix-nya.
Middleware buatan sendiri
- Logger: mencatat method, URL, status, dan durasi. Status baru diketahui setelah response selesai, jadi kita mendengarkan event
"finish"padares(ingat,resadalah EventEmitter). - Auth sederhana: memeriksa header
x-api-key. Ini contoh pola saja; autentikasi sungguhan (session, JWT, hash password) dibahas di bagian 2.
File statis
app.use(express.static("public")) menyajikan isi folder public apa adanya: public/index.html bisa diakses di /, public/logo.png di /logo.png. Di Express 5, file berawalan titik (dotfiles seperti .env) diabaikan secara default. Path folder bersifat relatif terhadap folder terminal, jadi untuk keamanan gunakan path.join(import.meta.dirname, "public").
Middleware bawaan dan pihak ketiga
Kamu tidak perlu menulis semua middleware sendiri. Express membawa beberapa, dan ekosistem npm menyediakan ribuan lainnya:
| Middleware | Asal | Fungsi |
|---|---|---|
express.json() |
bawaan | Parse body JSON ke req.body |
express.urlencoded() |
bawaan | Parse body form HTML (nama=x&umur=3) |
express.static(folder) |
bawaan | Menyajikan file statis |
cors |
npm | Header CORS (modul terakhir) |
helmet |
npm | Header keamanan seperti X-Content-Type-Options |
morgan / pino-http |
npm | Logger siap pakai |
Menulis logger dan auth sendiri di modul ini tujuannya agar kamu paham polanya. Di proyek sungguhan, pakai paket yang sudah teruji untuk hal umum, dan tulis sendiri hanya untuk aturan khas aplikasimu. Prinsipnya sama: setiap paket di atas hanyalah fungsi (req, res, next).
Sintaks path di Express 5
Express 5 memakai versi baru path-to-regexp, sehingga beberapa pola Express 4 tidak berlaku:
| Express 4 | Express 5 | Keterangan |
|---|---|---|
"/*" |
"/*splat" atau "/{*splat}" |
Wildcard wajib diberi nama; versi berkurung ikut mencocokkan / |
"/:file.:ext?" |
"/:file{.:ext}" |
Bagian opsional memakai kurung kurawal |
"/[a|b]/:slug" |
["/a/:slug", "/b/:slug"] |
Karakter regex di string tidak didukung |
Kalau kamu menulis app.get("/*", ...) di Express 5, aplikasi langsung gagal start dengan error Missing parameter name at index 2: /*. Nilai wildcard bernama berupa array segmen: request /docs/express/routing/params ke rute /docs/*jalur menghasilkan req.params.jalur = ["express", "routing", "params"].
Contoh kode
Struktur proyek:
m6/
├── .env # PORT=3000, API_KEY=rahasia123
├── server.js
├── middleware/
│ ├── logger.js
│ └── auth.js
├── routes/
│ └── produk.js
└── public/
└── index.html
// middleware/logger.js
export function logger(req, res, next) {
const mulai = performance.now();
// status baru diketahui setelah response terkirim
res.on("finish", () => {
const ms = (performance.now() - mulai).toFixed(1);
console.log(`${req.method} ${req.originalUrl} → ${res.statusCode} (${ms} ms)`);
});
next(); // jangan lupa, kalau tidak request berhenti di sini
}
// middleware/auth.js
export function wajibApiKey(req, res, next) {
const kunci = req.get("x-api-key");
if (kunci !== process.env.API_KEY) {
return res.status(401).json({ error: "API key salah atau tidak ada" }); // hentikan
}
req.user = { nama: "admin" }; // titipkan data untuk handler berikutnya
next();
}
// routes/produk.js
import { Router } from "express";
import { wajibApiKey } from "../middleware/auth.js";
const router = Router();
const produk = [{ id: 1, nama: "Kopi Susu", harga: 18000 }];
router.get("/", (req, res) => res.json(produk));
router.get("/:id", (req, res) => {
const item = produk.find((p) => p.id === Number(req.params.id));
if (!item) return res.status(404).json({ error: "Produk tidak ditemukan" });
res.json(item);
});
// middleware hanya untuk rute ini: GET tetap publik, POST butuh API key
router.post("/", wajibApiKey, (req, res) => {
const baru = { id: produk.length + 1, ...req.body };
produk.push(baru);
res.status(201).json({ dibuatOleh: req.user.nama, data: baru });
});
export default router;
// server.js, jalankan: node --env-file=.env server.js
import express from "express";
import produkRouter from "./routes/produk.js";
import { logger } from "./middleware/logger.js";
const app = express();
app.use(logger); // 1. catat semua request
app.use(express.json()); // 2. parse body JSON
app.use(express.static("public")); // 3. file statis
app.use("/api/produk", produkRouter); // 4. rute per fitur
app.get("/api/health", (req, res) =>
res.json({ status: "ok", uptime: Math.round(process.uptime()) })
);
// 5. 404 JSON: hanya tercapai kalau tidak ada yang cocok di atas
app.use((req, res) => {
res.status(404).json({ error: `Rute ${req.method} ${req.originalUrl} tidak ditemukan` });
});
const PORT = process.env.PORT ?? 3000;
app.listen(PORT, () => console.log(`Jalan di http://localhost:${PORT}`));
Uji:
curl -s localhost:3000/
# → <!doctype html>
# → <h1>Halo dari folder public</h1>
curl -s localhost:3000/api/produk
# → [{"id":1,"nama":"Kopi Susu","harga":18000}]
curl -s -w ' [%{http_code}]' -X POST localhost:3000/api/produk \
-H 'Content-Type: application/json' -d '{"nama":"Teh","harga":9000}'
# → {"error":"API key salah atau tidak ada"} [401]
curl -s -w ' [%{http_code}]' -X POST localhost:3000/api/produk \
-H 'Content-Type: application/json' -H 'x-api-key: rahasia123' -d '{"nama":"Teh","harga":9000}'
# → {"dibuatOleh":"admin","data":{"id":2,"nama":"Teh","harga":9000}} [201]
curl -s -w ' [%{http_code}]' localhost:3000/api/apa
# → {"error":"Rute GET /api/apa tidak ditemukan"} [404]
curl -s localhost:3000/api/health
# → {"status":"ok","uptime":1}
Log server dari middleware logger:
# → Jalan di http://localhost:3000
# → GET / → 200 (5.3 ms)
# → GET /api/produk → 200 (1.1 ms)
# → GET /api/produk/1 → 200 (0.4 ms)
# → POST /api/produk → 401 (2.8 ms)
# → POST /api/produk → 201 (0.3 ms)
# → GET /api/apa → 404 (0.2 ms)
# → GET /api/health → 200 (0.3 ms)
Contoh sintaks path Express 5 (diuji langsung):
app.get("/berkas/:nama{.:ext}", (req, res) => res.json(req.params));
app.get("/docs/*jalur", (req, res) => res.json({ jalur: req.params.jalur }));
// GET /berkas/laporan.pdf → {"nama":"laporan","ext":"pdf"}
// GET /berkas/laporan → {"nama":"laporan"}
// GET /docs/express/routing/params → {"jalur":["express","routing","params"]}
Kesalahan umum
- Middleware tidak memanggil
next()dan tidak menjawab. Request menggantung. Setiap cabang harus memilih salah satu. - Memasang handler 404 di atas rute. Semua request jadi 404. Handler 404 selalu di bawah semua rute.
- Memakai sintaks path Express 4 seperti
"/*"atau"/:id?". Di Express 5 aplikasi gagal start. Pakai"/*splat"dan"{/:id}". - Menaruh
express.json()setelah router. Router menerimareq.bodybernilaiundefined. - Menyajikan folder proyek sebagai static (
express.static(".")). Kode sumber dan file konfigurasi bisa ikut terbaca. Sajikan hanya folderpublickhusus.
Latihan
- Mudah, Buat middleware
waktuRequestyang menambahkanreq.waktu = new Date().toISOString(), lalu tampilkan diGET /api/health. - Sedang, Buat router
routes/kategori.jsdenganGET /danGET /:slug, lalu pasang di/api/kategori. - Menantang, Buat middleware pabrik
batasiRequest(maks, jendelaMs)yang membatasi tiap IP maksimalmaksrequest per jendela waktu. Kalau lewat, balas 429 dengan headerRetry-After(detik).
Lihat solusi
1.
function waktuRequest(req, res, next) {
req.waktu = new Date().toISOString();
next();
}
app.use(waktuRequest);
app.get("/api/health", (req, res) => res.json({ status: "ok", waktu: req.waktu }));
2.
// routes/kategori.js
import { Router } from "express";
const router = Router();
const kategori = [{ slug: "minuman", nama: "Minuman" }, { slug: "makanan", nama: "Makanan" }];
router.get("/", (req, res) => res.json(kategori));
router.get("/:slug", (req, res) => {
const k = kategori.find((x) => x.slug === req.params.slug);
if (!k) return res.status(404).json({ error: "Kategori tidak ditemukan" });
res.json(k);
});
export default router;
// server.js
// import kategoriRouter from "./routes/kategori.js";
// app.use("/api/kategori", kategoriRouter);
3.
export function batasiRequest(maks, jendelaMs) {
const catatan = new Map(); // ip → { jumlah, reset }
return (req, res, next) => {
const sekarang = Date.now();
let c = catatan.get(req.ip);
if (!c || sekarang > c.reset) {
c = { jumlah: 0, reset: sekarang + jendelaMs };
catatan.set(req.ip, c);
}
c.jumlah++;
if (c.jumlah > maks) {
res.set("Retry-After", String(Math.ceil((c.reset - sekarang) / 1000)));
return res.status(429).json({ error: "Terlalu banyak request, coba lagi nanti" });
}
next();
};
}
// app.use("/api", batasiRequest(100, 60_000)); // 100 request per menit per IP
Versi ini menyimpan data di memori satu proses, cukup untuk belajar. Di produksi dengan banyak server, penghitungnya biasanya disimpan di Redis.
Ringkasan
- Middleware adalah fungsi
(req, res, next)yang dijalankan berurutan; ia bisa mengubah request, meneruskan dengannext(), atau langsung menjawab. - Urutan pendaftaran menentukan alur: logger dan parser di atas, rute di tengah, 404 dan error handler di bawah.
express.Router()memecah rute per fitur dan dipasang di prefix denganapp.use("/prefix", router).- Middleware bisa dipasang global, per prefix, atau per rute (misalnya auth hanya untuk POST).
express.staticmenyajikan folder publik; Express 5 mengabaikan dotfiles secara default.- Sintaks path Express 5: wildcard wajib bernama (
/*splat), bagian opsional memakai{}.