Router dan struktur

Berkas rute modular dan tata letak proyek yang dapat diskalakan.

Daftar isi

Express menyediakan express.Router sebagai konstruk untuk mengelompokkan rute ke dalam modul terpisah yang kemudian dipasang pada aplikasi utama. Topik ini membahas pembuatan berkas rute modular, pemasangannya pada prefiks jalur tertentu, dan tata letak direktori yang lazim dipakai proyek Express berukuran menengah. Setelah membaca topik ini, sebuah aplikasi dengan banyak sumber daya dapat dipecah menjadi berkas rute yang mandiri tanpa kehilangan struktur URL yang konsisten.

express.Router sebagai modul rute#

express.Router() mengembalikan instance yang berperilaku seperti aplikasi Express mini. Router dapat memanggil router.get, router.post, router.use, dan menerima middleware seperti app, tetapi tidak punya metode listen. Router dirancang untuk dipasang ke aplikasi utama lewat app.use(prefix, router), sehingga seluruh rute di dalamnya berbagi prefiks yang sama.

Pemisahan ke berkas sendiri dilakukan dengan mengekspor instance router melalui module.exports. Berkas router-users.js berikut mendefinisikan tiga rute pada router yang diekspor.

js router-users.js
// tidak-dijalankan: butuh Express yang dipasang di /tmp/router-dan-struktur
const express = require('express');
const router = express.Router();

router.get('/', (req, res) => {
  res.json({ sumber: 'router-users', aksi: 'daftar' });
});

router.post('/', (req, res) => {
  res.status(201).json({ sumber: 'router-users', aksi: 'buat' });
});

router.get('/:id', (req, res) => {
  res.json({ sumber: 'router-users', aksi: 'detail', id: req.params.id });
});

module.exports = router;

Router kedua, router-catatan.js, mendefinisikan rute catatan yang akan dipasang bersarang di bawah sumber daya pengguna.

js router-catatan.js
// tidak-dijalankan: butuh Express yang dipasang di /tmp/router-dan-struktur
const express = require('express');
const router = express.Router();

router.get('/', (req, res) => {
  res.json({ sumber: 'router-catatan', aksi: 'daftar' });
});

router.get('/:catatanId', (req, res) => {
  res.json({
    sumber: 'router-catatan',
    aksi: 'detail',
    catatanId: req.params.catatanId,
  });
});

module.exports = router;

Berkas router-users.js dan router-catatan.js hanya mengekspor router dan tidak menjalankan server. Saat dimuat lewat require, modul tersebut mengembalikan instance router yang siap dipasang pada app. Penandaan // tidak-dijalankan pada baris pertama hanya menyatakan bahwa berkas tidak berdiri sendiri tanpa Express yang terpasang, bukan bahwa berkas tidak dapat dimuat.

Memasang router pada aplikasi utama#

Aplikasi utama memuat kedua router dengan require, lalu memasangnya pada prefiks jalur yang diinginkan. Pemasangan app.use('/users', usersRouter) membuat seluruh rute di dalam usersRouter berbagi prefiks /users. Prefiks boleh memuat parameter, sehingga app.use('/users/:userId/catatan', catatanRouter) mengaitkan catatan pada pengguna tertentu.

js server.js
// tidak-dijalankan: butuh Express yang dipasang di /tmp/router-dan-struktur
const express = require('express');
const usersRouter = require('./router-users');
const catatanRouter = require('./router-catatan');

const app = express();
app.use(express.json());

app.use('/users', usersRouter);
app.use('/users/:userId/catatan', catatanRouter);

app.use((req, res) => {
  res.status(404).json({ error: 'tidak ditemukan' });
});

const server = app.listen(0, async () => {
  const port = server.address().port;
  const a = await fetch(`http://127.0.0.1:${port}/users`);
  const b = await fetch(`http://127.0.0.1:${port}/users`, {
    method: 'POST',
  });
  const c = await fetch(`http://127.0.0.1:${port}/users/7`);
  const d = await fetch(`http://127.0.0.1:${port}/users/7/catatan`);
  const e = await fetch(`http://127.0.0.1:${port}/users/7/catatan/42`);
  console.log('GET /users ->', a.status, await a.text());
  console.log('POST /users ->', b.status, await b.text());
  console.log('GET /users/7 ->', c.status, await c.text());
  console.log('GET /users/7/catatan ->', d.status, await d.text());
  console.log('GET /users/7/catatan/42 ->', e.status, await e.text());
  server.close();
});
bash
# jalankan dari direktori topik
NODE_PATH=/tmp/router-dan-struktur/node_modules node server.js
text Keluaran
GET /users -> 200 {"sumber":"router-users","aksi":"daftar"}
POST /users -> 201 {"sumber":"router-users","aksi":"buat"}
GET /users/7 -> 200 {"sumber":"router-users","aksi":"detail","id":"7"}
GET /users/7/catatan -> 200 {"sumber":"router-catatan","aksi":"daftar"}
GET /users/7/catatan/42 -> 200 {"sumber":"router-catatan","aksi":"detail","catatanId":"42"}

Keluaran memperlihatkan bagaimana prefiks menyatu dengan jalur di dalam router. Rute router.get('/') pada usersRouter menjadi GET /users, sedangkan router.get('/:id') menjadi GET /users/:id. Pada catatanRouter yang dipasang di /users/:userId/catatan, rute router.get('/:catatanId') menjadi GET /users/:userId/catatan/:catatanId, dan parameter :userId tidak dibawa ke dalam router catatan karena sudah ditangkap oleh prefiks.

Polanya berlaku untuk seluruh method. POST /users ditangani oleh router.post('/') pada usersRouter, bukan oleh handler pada app utama. Hal ini membuat pengujian dapat dijalankan terhadap satu router dalam isolasi: muat usersRouter dengan express(), pasang sebagai app.use(usersRouter), lalu tembak permintaan tanpa perlu menyentuh rute lain.

Tata letak proyek yang dapat diskalakan#

Pemisahan rute ke berkas sendiri menjadi titik awal tata letak proyek yang dapat tumbuh. Tata letak yang lazim memisahkan titik masuk aplikasi, definisi rute, dan logika domain ke dalam direktori berbeda. Pemisahan ini menjaga agar berkas tetap pendek dan memudahkan navigasi saat jumlah sumber daya bertambah.

text Contoh: tata letak direktori Express
// tidak-dijalankan: pohon direktori, bukan berkas utuh
proyek/
├── package.json
├── src/
│   ├── app.js              // membuat app Express, pasang middleware umum
│   ├── server.js           // memuat app, menjalankan app.listen
│   ├── routes/
│   │   ├── users.js        // router untuk /users
│   │   ├── catatan.js      // router untuk /users/:userId/catatan
│   │   └── index.js        // menggabungkan seluruh router
│   ├── middleware/
│   │   ├── auth.js
│   │   └── error.js
│   └── controllers/
│       ├── users.js
│       └── catatan.js
└── test/
    └── users.test.js

Direktori routes/ berisi definisi rute murni, yaitu pemetaan method dan jalur ke controller. Direktori controllers/ berisi fungsi handler yang mengambil req dan res, melakukan validasi, memanggil logika domain, dan mengirim respons. Pemisahan rute dan controller menjaga berkas rute tetap ringkas, sebab handler bisnis dapat tumbuh tanpa memuai definisi rute.

Direktori middleware/ menyimpan middleware yang dapat dipakai ulang seperti pencatat, pemeriksa token, dan penanganan error. Berkas routes/index.js berfungsi sebagai titik agregasi: ia memuat seluruh router dari berkas di routes/ dan mengekspornya sebagai satu modul yang siap dipasang pada app.

Pemisahan app.js dan server.js memungkinkan pengujian memuat app tanpa membuka socket. Berkas app.js hanya membuat dan mengekspor app, sedangkan server.js memanggil app.listen. Pengujian HTTP memakai app secara langsung atau lewat pustaka seperti supertest, tanpa perlu memilih port bebas secara manual.

Kesalahan umum#

Menggunakan app sebagai wadah tunggal untuk seluruh rute. Seiring bertambahnya sumber daya, app.js menjadi ratusan baris yang mencampur rute, middleware, dan logika bisnis. Pecah menjadi express.Router per sumber daya, dan pasang masing-masing pada prefiksnya di app.js.

Lupa mengaktifkan mergeParams saat router butuh parameter dari prefiks. Tanpa express.Router({ mergeParams: true }), handler di dalam router tidak dapat membaca req.params.userId yang ditangkap oleh prefiks. Gejalanya adalah req.params.userId bernilai undefined meskipun URL memuat segmen tersebut.

Mengandalkan urutan deklarasi rute di dalam router untuk penanganan 404. Middleware 404 sebaiknya dipasang pada level aplikasi (app.use) setelah seluruh router dipasang, bukan pada setiap router. Hal ini menjaga konsistensi respons 404 untuk semua jalur yang tidak terdaftar.

Ringkasan#

  • express.Router adalah modul rute yang dapat dipasang ke aplikasi lewat app.use(prefix, router) dan diekspor dari berkas sendiri.
  • Prefiks boleh memuat parameter, dan parameter tersebut hanya terlihat dari dalam router bila express.Router({ mergeParams: true }) dipakai.
  • Tata letak proyek yang lazim memisahkan app.js, server.js, routes/, middleware/, dan controllers/, sehingga app dapat dimuat tanpa membuka socket untuk pengujian.
  • Middleware 404 dipasang pada level aplikasi setelah seluruh router, agar respons 404 konsisten untuk semua jalur yang tidak terdaftar.

Referensi#