kodingup.camp

Beranda/Node.js Backend: Dasar, Express & REST API

Routing & Middleware

Modul 6 dari 105 menit bacaRouting and Middleware

Isi modul

    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:

    1. Melakukan sesuatu pada req/res (menambah req.user, misalnya),
    2. Meneruskan paket ke petugas berikutnya dengan memanggil next(), atau
    3. 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.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

    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

    1. Middleware tidak memanggil next() dan tidak menjawab. Request menggantung. Setiap cabang harus memilih salah satu.
    2. Memasang handler 404 di atas rute. Semua request jadi 404. Handler 404 selalu di bawah semua rute.
    3. Memakai sintaks path Express 4 seperti "/*" atau "/:id?". Di Express 5 aplikasi gagal start. Pakai "/*splat" dan "{/:id}".
    4. Menaruh express.json() setelah router. Router menerima req.body bernilai undefined.
    5. Menyajikan folder proyek sebagai static (express.static(".")). Kode sumber dan file konfigurasi bisa ikut terbaca. Sajikan hanya folder public khusus.

    Latihan

    1. Mudah, Buat middleware waktuRequest yang menambahkan req.waktu = new Date().toISOString(), lalu tampilkan di GET /api/health.
    2. Sedang, Buat router routes/kategori.js dengan GET / dan GET /:slug, lalu pasang di /api/kategori.
    3. Menantang, Buat middleware pabrik batasiRequest(maks, jendelaMs) yang membatasi tiap IP maksimal maks request per jendela waktu. Kalau lewat, balas 429 dengan header Retry-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

    Video pendamping

    Kalau penjelasan di atas belum masuk, tonton ini. Video dari kanal YouTube pihak lain.

    Learn Express Middleware In 14 MinutesWeb Dev Simplified · Bahasa InggrisBuka di YouTube

    Kuis pemahaman

    Pilih satu jawaban per soal. Penjelasan muncul setelah kamu menjawab.