Menyajikan aset statis dengan express.static dan merender halaman server-side dengan EJS dan Pug, termasuk keamanan template dan struktur views yang rapi.

Sejauh ini kalian membangun API yang mengembalikan JSON. Episode 11 menjawab sisi lain aplikasi web: menyajikan halaman dan aset. Banyak sistem produksi — dashboard admin, halaman marketing, aplikasi monolitik — butuh Express untuk merender HTML langsung dari server.
Mengapa ini penting di 2026, di tengah popularitas SPA? Tiga alasan: SEO (HTML server-side langsung ter-index), performansi awal (tanpa menunggu bundle JavaScript), dan aplikasi internal yang tidak butuh kompleksitas frontend terpisah. Express menangani keduanya dengan express.static untuk aset dan template engine untuk halaman.
express.static() (dikenalkan di episode 5) menyajikan file apa adanya:
import express from "express"
import path from "node:path"
const app = express()
app.use(express.static(path.join(process.cwd(), "public")))Struktur direktori public/:
public/
css/style.css
js/app.js
images/logo.svgFile public/css/style.css lalu bisa diakses di http://localhost:3000/css/style.css — tanpa prefix tambahan.
Untuk memisahkan aset publik dari file lain, gunakan prefix:
app.use("/static", express.static("public"))Sekarang aset diakses lewat http://localhost:3000/static/css/style.css. Prefix ini tidak perlu ada di filesystem — ia murni "label" URL, sehingga kalian bisa merestrukturisasi folder tanpa memutus link client.
Note
express.static juga otomatis mengirim ETag dan header cache dasar. Di episode 21 kita akan mengoptimalkan header Cache-Control untuk aset yang tidak berubah.
EJS menggunakan sintaks HTML dengan tag <% %> untuk logika:
npm install ejsconst app = express()
app.set("view engine", "ejs")
app.set("views", path.join(process.cwd(), "views"))app.set("view engine", "ejs") memberi tahu Express template mana yang dipakai; app.set("views", ...) menentukan lokasi file template.
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8" />
<title><%= title %></title>
<link rel="stylesheet" href="/static/css/style.css" />
</head>
<body>
<h1><%= title %></h1>
<ul>
<% users.forEach((user) => { %>
<li><%= user.name %> - <%= user.email %></li>
<% }) %>
</ul>
</body>
</html>Tag <%= %> meng-escape output HTML otomatis — inilah pertahanan pertama terhadap XSS. Data dikirim dari handler lewat res.render.
export const renderUsers = async (req, res) => {
const users = await User.find({}).lean()
res.render("index", {
title: "Daftar User",
users,
})
}res.render("index", data) menggabungkan template index.ejs dengan data, lalu mengirim HTML jadi. Data yang dipassarkan tersedia sebagai variabel di template.
Pug (sebelumnya Jade) memakai sintaks indentasi tanpa tag penutup — lebih ringkas tapi lebih kaku:
npm install pugapp.set("view engine", "pug")
app.set("views", path.join(process.cwd(), "views"))doctype html
html(lang="id")
head
meta(charset="utf-8")
title= title
link(rel="stylesheet", href="/static/css/style.css")
body
h1= title
ul
each user in users
li #{user.name} - #{user.email}Pug mengeksekusi JavaScript inline untuk logika (each ... in), dan interpolasi #{...} juga di-escape. Pilih EJS jika ingin template yang mirip HTML biasa; Pug jika menyukai sintaks minimal.
Kesalahan paling serius di templating adalah menampilkan input pengguna tanpa escape — celah XSS:
<%# BENAR: di-escape, aman terhadap XSS %>
<p><%= user.name %></p>
<%# SALAH: raw, XSS jika name berisi script %>
<p><%- user.name %></p>Tag <%- %> (tanpa = yang double dengan -) tidak meng-escape — hanya boleh dipakai untuk konten yang kalian percaya sepenuhnya, seperti HTML yang dihasilkan sendiri.
Warning
Perbedaan <%= %> (escape) dan <%- %> (raw) adalah garis tipis antara aman dan XSS. Aturan: input pengguna selalu lewat <%= %>. Pengecualian raw hanya untuk konten internal yang sudah disanitasi. Detail lengkap XSS dibahas di episode 18.
Jangan pernah merender data dari req.query langsung ke template tanpa escape — nilai query adalah input pengguna.
app.set("views", "views") bergantung pada working directory saat node dijalankan. Gunakan path.join(process.cwd(), "views") agar bekerja di mana pun server dipanggil — penting saat deployment (episode 24).
Express melempar error "Cannot find module 'ejs'" jika engine tidak terinstall. npm install ejs harus jalan sebelum app.set("view engine", "ejs") berguna.
Jika ada file public/users.html, request GET /users.html dilayani static — bukan route. Simpan aset dengan ekstensi jelas dan gunakan prefix /static bila perlu menghindari tabrakan nama.
Tip
Untuk aplikasi yang juga menyajikan API, pisahkan dengan jelas: /api/* untuk JSON (tanpa session template), sisanya untuk halaman. Pola ini menjaga endpoint tetap murni dan mudah diuji.
Episode 11 menambahkan wajah kedua aplikasi web: express.static untuk aset dengan virtual path, EJS dan Pug untuk rendering server-side, dan aturan keamanan template yang mencegah XSS.
Inti yang harus dibawa pulang:
express.static("public") menyajikan aset; virtual prefix (/static) memisahkan namespace.<%= %>) dan Pug (#{...}) sama-sama meng-escape output secara default.res.render("index", data) menggabungkan template dan data menjadi HTML.<%- %> (raw) hanya untuk konten tepercaya — input pengguna selalu <%= %>.path.join(process.cwd(), ...) agar portabel.Di episode 12 selanjutnya kita akan mengamankan aplikasi dengan authentication & authorization — bcrypt, JWT dan session cookie, middleware auth, serta RBAC dengan peran. Sampai jumpa di episode 12!