Membedah express.Router, cara memecah aplikasi menjadi route modular per resource, mounting di app, dan refactor aplikasi Hello World menjadi struktur routes controllers dan middleware yang bersih.

Setelah di episode 7 error handling terpusat berdiri, aplikasi kalian butuh organisasi. Episode 8 memperkenalkan express.Router() — alat untuk memecah satu app.js yang makin panjang menjadi modul-modul route yang mandiri.
Mengapa ini penting? Bayangkan aplikasi dengan route users, products, orders, dan auth — jika semua ditulis di app.js, file itu akan membengkak hingga ribuan baris dan konflik git menjadi mimpi buruk. Router memungkinkan tiap resource dirawat di file sendiri, diuji sendiri, dan bahkan dipakai ulang di mount point berbeda. Ini juga refactor pertama yang mengaktifkan struktur folder yang kita siapkan di episode 3.
Router adalah "mini-app": ia punya get, post, use, dan seluruh API route seperti app, tapi bukan server yang berdiri sendiri:
import { Router } from "express"
const router = Router()
router.get("/", (req, res) => {
res.json({ data: [], path: "/users" })
})
router.get("/:id", (req, res) => {
res.json({ id: req.params.id })
})
router.post("/", (req, res) => {
res.status(201).json(req.body)
})
export default routerPerhatikan path di dalam router relatif terhadap mount point. router.get("/") di sini berarti GET /users setelah router di-mount di /users. Ini kunci menghindari duplikasi prefix.
Router dihubungkan ke app dengan app.use():
import express from "express"
import usersRouter from "./routes/users.js"
const app = express()
app.use(express.json())
app.use("/users", usersRouter)
app.use((req, res) => {
res.status(404).json({ error: "NOT_FOUND", message: "Route tidak ditemukan" })
})
export default appSekarang seluruh route di usersRouter hidup di bawah prefix /users. Mengubah prefix hanya perlu satu baris — router itu sendiri tidak tahu (dan tidak perlu tahu) di mana ia dipasang.
Struktur yang kita siapkan di episode 3 kini terisi. Pola routes + controllers memisahkan definisi URL dari logika:
const users = []
export const listUsers = (req, res) => {
res.json({ data: users })
}
export const getUser = (req, res) => {
const user = users.find((u) => u.id === Number(req.params.id))
if (!user) {
const err = new Error("User tidak ditemukan")
err.status = 404
throw err
}
res.json({ data: user })
}
export const createUser = (req, res) => {
const user = { id: users.length + 1, ...req.body }
users.push(user)
res.status(201).json({ data: user })
}Controller adalah fungsi murni (req, res) yang menangani satu aksi — tidak tahu menahu soal URL. Inilah unit yang bisa diuji langsung tanpa server (ujian automated dibahas di episode 27).
import { Router } from "express"
import { listUsers, getUser, createUser } from "../controllers/userController.js"
const router = Router()
router.get("/", listUsers)
router.get("/:id", getUser)
router.post("/", createUser)
export default routerRoute kini hanya peta: path dan metode dipasangkan ke controller. Logika berada di controller, mudah dibaca dan diuji.
Menambah resource tinggal mengulang pola yang sama:
import usersRouter from "./routes/users.js"
import productsRouter from "./routes/products.js"
app.use("/users", usersRouter)
app.use("/products", productsRouter)Setiap resource mandiri. Ini contoh nyata dari filosofi unopinionated Express — tidak ada framework yang memaksa struktur ini, tapi pola routes/controllers ini adalah standar industri yang paling banyak dipakai.
Router juga bisa punya middleware khusus, misalnya validasi hanya untuk route users:
const router = Router()
router.use((req, res, next) => {
console.log(`users router: ${req.method} ${req.path}`)
next()
})Middleware yang dipasang router.use() hanya berjalan untuk request yang masuk ke router tersebut — tidak untuk route lain di app. Ini berguna untuk logging spesifik resource atau autentikasi grup route (episode 12).
Note
Perbedaan req.path di dalam router: karena router dipasang di /users, req.path di dalam handler router mengembalikan bagian setelah mount point (misal /42), sedangkan req.originalUrl selalu URL lengkap. Gunakan sesuai kebutuhan — keduanya sering tertukar.
Menulis router.get("/users") di router yang di-mount di /users menghasilkan /users/users. Di dalam router, path selalu relatif — jangan ulangi prefix.
Router yang tidak di-export membuat app.use("/users", usersRouter) gagal saat import. Selalu export default router di akhir file router.
router.use((req, res) => ...) untuk 404 hanya menangkap request di dalam prefix router tersebut. 404 global harus tetap didefinisikan di app.js setelah semua mount.
Tip
Jadikan pola "routes = peta, controllers = logika" sebagai kebiasaan tetap. Saat episode 9 dan 10 menambahkan database, kalian cukup menambah lapisan model — routes dan controllers nyaris tidak berubah.
Episode 8 memecah aplikasi monolitik menjadi modul: express.Router() membungkus route per resource, app.use(prefix, router) menempelkannya ke app, dan pola routes/controllers memisahkan peta URL dari logika bisnis.
Inti yang harus dibawa pulang:
express.Router() adalah mini-app: route di dalamnya relatif terhadap mount point.app.use("/users", usersRouter); ganti prefix cukup satu baris.app.js, bukan di router.Di episode 9 selanjutnya kita akan menambah lapisan database pertama: CRUD dengan MongoDB dan Mongoose — schema, model, koneksi, dan REST CRUD lengkap. Sampai jumpa di episode 9!