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:
req(IncomingMessage) berisireq.method,req.url(path + query, misalnya/tugas?q=node), danreq.headers(objek dengan nama header huruf kecil).reqjuga sebuah Readable stream untuk body.res(ServerResponse) dipakai untuk menjawab:res.statusCode,res.setHeader(nama, nilai),res.writeHead(status, headers),res.write(data), dan wajib diakhirires.end(data).
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
- Lupa
res.end(). Request menggantung sampai timeout. Setiap cabangifharus berakhir dengan mengirim response, dan pakaireturnsupaya kode di bawahnya tidak ikut jalan. - Mengirim response dua kali. Tanpa
return, alur bisa jatuh kekirimJSONberikutnya dan memicu errorERR_HTTP_HEADERS_SENT. - Tidak menangani JSON rusak.
JSON.parsemelempar error; tanpatry/catch, server mengembalikan error mentah atau proses bisa crash kalau error itu tidak tertangkap. - Membandingkan
req.urllangsung dengan path.req.urlikut membawa query, jadi/tugas?q=xtidak sama dengan/tugas. Urai dulu dengannew URL. - Lupa header
Content-Type. Client (termasukfetchdanres.json()di React) bisa salah menafsirkan isi response.
Latihan
- Mudah, Tambahkan rute
GET /waktuyang mengembalikan{ "sekarang": "<waktu ISO>" }. - Sedang, Tambahkan rute
DELETE /tugas/:idyang menghapus tugas dan mengembalikan status 204 tanpa body, atau 404 kalau tidak ada. - Menantang, Batasi ukuran body maksimal 1 KB. Kalau lebih, balas
413 Payload Too Largedan 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
http.createServer((req, res) => ...)memanggil handler untuk setiap request.reqberisi method, url, headers, dan body sebagai stream;resdipakai untuk status, header, dan isi, dan wajib diakhirires.end().- Urai URL dengan
new URL(req.url, base)untuk mendapatkanpathnamedansearchParams. - Mengirim JSON berarti
JSON.stringifyditambah headerContent-Type: application/json. - Routing manual cepat jadi berantakan; framework seperti Express menyediakan routing, parsing body, middleware, dan error handling terpusat.