kodingup.camp

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

Server HTTP Murni

Modul 4 dari 104 menit bacaHTTP Server from Scratch

Isi modul

    Kenapa ini penting

    Express, Fastify, Hono, bahkan Next.js di mode server, semuanya pada akhirnya berdiri di atas modul node:http. Kalau kamu pernah membuat server dengan tangan kosong sekali saja, kamu akan paham apa yang sebenarnya dikerjakan framework untukmu: membaca body, mencocokkan URL, mengatur header, menangani error. Kamu juga jadi tahu kenapa res.json() itu ada, dan apa yang harus dicek ketika framework berperilaku aneh.

    Anggap modul ini seperti belajar menyetir mobil manual sebelum pindah ke matik. Kamu tidak akan memakainya setiap hari, tapi pemahamannya menempel.

    Konsep inti

    createServer dan siklus request

    http.createServer(handler) membuat server. Fungsi handler(req, res) dipanggil setiap kali ada request masuk:

    Kalau kamu lupa res.end(), client akan menunggu terus sampai timeout. Analoginya pelayan yang sudah mengambil piring tapi tidak pernah mengantarnya ke meja.

    server.listen(3000) mulai mendengarkan di port 3000. Port adalah "nomor pintu" di komputer; satu port hanya bisa dipakai satu program. Kalau sudah dipakai, kamu akan melihat error EADDRINUSE.

    Membaca URL dan query

    req.url hanya berisi path dan query. Untuk mengurainya, gunakan kelas URL bawaan (sama seperti di browser): new URL(req.url, "http://" + req.headers.host). Lalu url.pathname berisi path, dan url.searchParams.get("q") mengambil query.

    Mengirim JSON

    Response hanyalah teks atau byte. Untuk mengirim JSON, kamu harus melakukan dua hal sendiri: mengubah objek menjadi string dengan JSON.stringify, dan memberi header Content-Type: application/json; charset=utf-8 agar client tahu cara membacanya.

    Membaca body

    Body datang sebagai stream potongan-potongan. Kamu harus mengumpulkannya, lalu mem-parse JSON, dan menangani kemungkinan JSON rusak. Karena req adalah async iterable, cara modern adalah for await (const potongan of req).

    Routing manual

    Routing artinya memilih kode mana yang dijalankan berdasarkan method dan path. Tanpa framework, ini berupa deretan if. Untuk path dengan parameter seperti /tugas/5, kamu memakai regex.

    Membaca header response

    Jalankan curl -i ke server ini dan kamu akan melihat beberapa header yang tidak kamu tulis sendiri: Date, Connection: keep-alive, Keep-Alive: timeout=5, dan Transfer-Encoding: chunked. Node menambahkannya otomatis. keep-alive berarti koneksi TCP tidak langsung ditutup setelah satu response, sehingga request berikutnya dari client yang sama bisa memakai koneksi yang sama dan lebih cepat. chunked berarti panjang body tidak diumumkan di depan (karena kita tidak mengirim header Content-Length); body dikirim per potongan dan diakhiri penanda khusus. Kebiasaan membaca header seperti ini akan sangat membantu saat kamu men-debug masalah cache, CORS, atau cookie nanti.

    Satu proses, banyak request

    Array tugas di contoh ini dibuat sekali saat server start, lalu dipakai bersama oleh semua request. Itu sebabnya POST dari satu client langsung terlihat oleh GET client lain. Ini juga berarti dua hal. Pertama, data hilang saat proses berhenti, karena hanya tersimpan di memori. Kedua, kamu tidak boleh menyimpan data milik satu pengguna di variabel global seperti let userSekarang, karena request pengguna lain akan menimpanya. Data per request selalu disimpan di variabel lokal di dalam handler atau ditempel ke objek req.

    Kenapa butuh framework

    Lihat contoh di bawah: sekitar 50 baris hanya untuk empat rute sederhana. Bayangkan 40 rute, ditambah logging, autentikasi, CORS, file statis, upload, dan error handling di setiap rute. Kodenya akan penuh dengan pengulangan. Framework seperti Express memberi:

    Masalah di node:http Solusi di Express
    if bertumpuk untuk routing app.get("/tugas/:id", ...)
    Parse body manual express.json()
    JSON.stringify + header manual res.json(data)
    Kode berulang di tiap rute (log, auth) Middleware
    Error di tiap rute ditangani sendiri Error handler terpusat

    Contoh kode

    // http-server.js, jalankan: node http-server.js
    import http from "node:http";
    
    const tugas = [
      { id: 1, judul: "Belajar Node", selesai: false },
      { id: 2, judul: "Belajar Express", selesai: false },
    ];
    
    // Helper: kirim JSON dengan status & header yang benar
    function kirimJSON(res, status, data) {
      res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
      res.end(JSON.stringify(data));
    }
    
    // Helper: kumpulkan potongan body lalu parse JSON
    async function bacaBody(req) {
      let teks = "";
      for await (const potongan of req) teks += potongan;
      return teks ? JSON.parse(teks) : {};
    }
    
    const server = http.createServer(async (req, res) => {
      const url = new URL(req.url, `http://${req.headers.host}`);
      console.log(req.method, url.pathname);
    
      if (req.method === "GET" && url.pathname === "/") {
        return kirimJSON(res, 200, { pesan: "Halo dari node:http" });
      }
    
      if (req.method === "GET" && url.pathname === "/tugas") {
        const cari = url.searchParams.get("q")?.toLowerCase();
        const hasil = cari ? tugas.filter((t) => t.judul.toLowerCase().includes(cari)) : tugas;
        return kirimJSON(res, 200, hasil);
      }
    
      const cocok = url.pathname.match(/^\/tugas\/(\d+)$/); // /tugas/123
      if (req.method === "GET" && cocok) {
        const item = tugas.find((t) => t.id === Number(cocok[1]));
        return item ? kirimJSON(res, 200, item) : kirimJSON(res, 404, { error: "Tugas tidak ditemukan" });
      }
    
      if (req.method === "POST" && url.pathname === "/tugas") {
        try {
          const body = await bacaBody(req);
          const baru = { id: tugas.length + 1, judul: body.judul, selesai: false };
          tugas.push(baru);
          return kirimJSON(res, 201, baru);
        } catch {
          return kirimJSON(res, 400, { error: "Body bukan JSON yang valid" });
        }
      }
    
      kirimJSON(res, 404, { error: `Rute ${req.method} ${url.pathname} tidak ada` });
    });
    
    server.listen(3000, () => console.log("Server jalan di http://localhost:3000"));
    

    Uji dari terminal lain:

    curl -s localhost:3000/
    # → {"pesan":"Halo dari node:http"}
    
    curl -s "localhost:3000/tugas?q=express"
    # → [{"id":2,"judul":"Belajar Express","selesai":false}]
    
    curl -s localhost:3000/tugas/9
    # → {"error":"Tugas tidak ditemukan"}
    
    curl -s -X POST localhost:3000/tugas -H 'Content-Type: application/json' -d '{"judul":"Belajar REST"}'
    # → {"id":3,"judul":"Belajar REST","selesai":false}
    
    curl -s -X POST localhost:3000/tugas -d 'bukan json'
    # → {"error":"Body bukan JSON yang valid"}
    
    curl -s -X DELETE localhost:3000/tugas/1
    # → {"error":"Rute DELETE /tugas/1 tidak ada"}
    
    curl -si localhost:3000/ | head -3
    # → HTTP/1.1 200 OK
    # → Content-Type: application/json; charset=utf-8
    # → Date: Fri, 23 Oct 2026 18:09:08 GMT
    

    Log di terminal server:

    # → Server jalan di http://localhost:3000
    # → GET /
    # → GET /tugas
    # → GET /tugas/9
    # → POST /tugas
    # → POST /tugas
    # → DELETE /tugas/1
    

    Perhatikan: log menampilkan /tugas, bukan /tugas?q=express, karena kita mencetak pathname. Perhatikan juga bahwa POST dengan judul kosong tetap diterima. Server ini belum punya validasi; itu yang akan kita perbaiki dengan Zod nanti.

    Kesalahan umum

    1. Lupa res.end(). Request menggantung sampai timeout. Setiap cabang if harus berakhir dengan mengirim response, dan pakai return supaya kode di bawahnya tidak ikut jalan.
    2. Mengirim response dua kali. Tanpa return, alur bisa jatuh ke kirimJSON berikutnya dan memicu error ERR_HTTP_HEADERS_SENT.
    3. Tidak menangani JSON rusak. JSON.parse melempar error; tanpa try/catch, server mengembalikan error mentah atau proses bisa crash kalau error itu tidak tertangkap.
    4. Membandingkan req.url langsung dengan path. req.url ikut membawa query, jadi /tugas?q=x tidak sama dengan /tugas. Urai dulu dengan new URL.
    5. Lupa header Content-Type. Client (termasuk fetch dan res.json() di React) bisa salah menafsirkan isi response.

    Latihan

    1. Mudah, Tambahkan rute GET /waktu yang mengembalikan { "sekarang": "<waktu ISO>" }.
    2. Sedang, Tambahkan rute DELETE /tugas/:id yang menghapus tugas dan mengembalikan status 204 tanpa body, atau 404 kalau tidak ada.
    3. Menantang, Batasi ukuran body maksimal 1 KB. Kalau lebih, balas 413 Payload Too Large dan hentikan pembacaan.
    Lihat solusi

    1.

    if (req.method === "GET" && url.pathname === "/waktu") {
      return kirimJSON(res, 200, { sekarang: new Date().toISOString() });
    }
    

    2. Letakkan sebelum 404 terakhir:

    const hapus = url.pathname.match(/^\/tugas\/(\d+)$/);
    if (req.method === "DELETE" && hapus) {
      const index = tugas.findIndex((t) => t.id === Number(hapus[1]));
      if (index === -1) return kirimJSON(res, 404, { error: "Tugas tidak ditemukan" });
      tugas.splice(index, 1);
      res.writeHead(204);
      return res.end();
    }
    

    3.

    class BodyTerlaluBesar extends Error {}
    
    async function bacaBody(req, batas = 1024) {
      let teks = "";
      for await (const potongan of req) {
        teks += potongan;
        if (Buffer.byteLength(teks) > batas) throw new BodyTerlaluBesar();
      }
      return teks ? JSON.parse(teks) : {};
    }
    
    // di dalam handler POST:
    try {
      const body = await bacaBody(req);
      // ...
    } catch (err) {
      if (err instanceof BodyTerlaluBesar) return kirimJSON(res, 413, { error: "Body maksimal 1 KB" });
      return kirimJSON(res, 400, { error: "Body bukan JSON yang valid" });
    }
    

    Ringkasan

    Video pendamping

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

    How to Build a Web Server with Node JS | Node.js Tutorials for BeginnersDave Gray · Bahasa InggrisBuka di YouTube

    Kuis pemahaman

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