Express adalah pilihan aman, tetapi bukan satu-satunya. Fastify lahir dengan tiga fokus desain: performa tinggi, validasi berbasis skema JSON, dan arsitektur plugin yang rapi. Hasilnya adalah framework yang terasa berbeda dari Express — handler berupa async function yang cukup mengembalikan nilai, bukan menulis respons secara manual. Pada Chapter ini kita akan membangun server Fastify pertama, memakai skema JSON untuk memvalidasi input, lalu menyusun error handler dan plugin terpusat.
Server Fastify Pertama #
Sebuah aplikasi Fastify dimulai dari function Fastify() yang mengembalikan instance aplikasi. Rute didaftarkan dengan method yang mirip Express, tetapi handler-nya async dan nilai baliknya otomatis menjadi body respons.
import Fastify from 'fastify';
const host = '127.0.0.1';
const app = Fastify();
app.get('/', async () => ({ pesan: 'Selamat datang di Fastify' }));
app.get('/halo/:nama', async (req) => ({ pesan: `Halo, ${req.params.nama}` }));
await app.listen({ port: 0, host });
const base = `http://${host}:${app.server.address().port}`;
const r1 = await fetch(`${base}/`);
console.log('GET / :', r1.status, await r1.text());
const r2 = await fetch(`${base}/halo/Siti`);
console.log('GET /halo :', r2.status, await r2.text());
await app.close();Output yang Dihasilkan:
GET / : 200 {"pesan":"Selamat datang di Fastify"}
GET /halo : 200 {"pesan":"Halo, Siti"}Empat perbedaan dengan Express terlihat jelas pada berkas pendek ini:
- Baris 6 dan 8 (async handler): handler tidak menerima
res. Object yang dikembalikan handler otomatis di-serialize menjadi JSON — pola ini membuat handler mudah diuji sebagai function biasa karena kita tinggal memanggilnya dan memeriksa nilai baliknya. - Baris 10 (
await app.listen(...)):listenmengembalikan promise, sehingga proses menunggu server benar-benar siap sebelum baris berikutnya dijalankan. Kita tidak perlu callback seperti pada Express. - Baris 11 (
app.server.address().port): instance Fastify membungkus server HTTP asli di propertiapp.server, tempat kita membaca port ephemeral untuk pengujian. - Baris 18 (
await app.close()): penutupan server juga berbasis promise — Fastify menghentikan plugin dan koneksi secara tertib sebelum promise selesai.
Validasi dengan Skema JSON #
Fitur paling khas Fastify adalah validasi bawaan. Setiap rute dapat membawa JSON Schema untuk body, query string, dan params; permintaan yang tidak cocok ditolak dengan status 400 sebelum handler sempat berjalan. Bandingkan dengan Express yang memerlukan package validasi terpisah.
import Fastify from 'fastify';
const host = '127.0.0.1';
const app = Fastify();
const skemaBuat = {
schema: {
body: {
type: 'object',
required: ['judul'],
properties: {
judul: { type: 'string', minLength: 3 },
selesai: { type: 'boolean' },
},
},
},
};
app.post('/tugas', skemaBuat, async (req) => ({
diterima: { judul: req.body.judul, selesai: req.body.selesai ?? false },
}));
app.get('/tugas', {
schema: {
querystring: {
type: 'object',
properties: { halaman: { type: 'integer' } },
},
},
}, async (req) => ({
halaman: req.query.halaman ?? 1,
jenis: typeof req.query.halaman,
}));
await app.listen({ port: 0, host });
const base = `http://${host}:${app.server.address().port}`;
const r1 = await fetch(`${base}/tugas`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ judul: 'Belajar Fastify', selesai: false }),
});
console.log('valid :', r1.status, await r1.text());
const r2 = await fetch(`${base}/tugas`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ judul: 'ab' }),
});
console.log('judul pendek :', r2.status, await r2.text());
const r3 = await fetch(`${base}/tugas`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ selesai: true }),
});
console.log('tanpa judul :', r3.status, await r3.text());
const r4 = await fetch(`${base}/tugas?halaman=3`);
console.log('query integer :', r4.status, await r4.text());
const r5 = await fetch(`${base}/tugas?halaman=tiga`);
console.log('query bukan angka:', r5.status, await r5.text());
await app.close();Output yang Dihasilkan:
valid : 200 {"diterima":{"judul":"Belajar Fastify","selesai":false}}
judul pendek : 400 {"statusCode":400,"code":"FST_ERR_VALIDATION","error":"Bad Request","message":"body/judul must NOT have fewer than 3 characters"}
tanpa judul : 400 {"statusCode":400,"code":"FST_ERR_VALIDATION","error":"Bad Request","message":"body must have required property 'judul'"}
query integer : 200 {"halaman":3,"jenis":"number"}
query bukan angka: 400 {"statusCode":400,"code":"FST_ERR_VALIDATION","error":"Bad Request","message":"querystring/halaman must be integer"}Perhatikan perilaku berikut dari keluaran di atas:
- Baris 6-17 (
skemaBuat): skema body menyatakanjudulwajib berupa string minimal tiga karakter danselesaiopsional bertipe boolean. Keluaranjudul pendekdantanpa judulmenunjukkan dua pelanggaran berbeda — semuanya ditolak 400 dengan kodeFST_ERR_VALIDATIONtanpa satu baris kode validasi pun di handler. - Baris 23-29 (skema querystring): skema query menyatakan
halamanbertipe integer. Keluaranquery integermemperlihatkan nilai"3"pada URL berubah menjadi angka3— itulah mengapajenisbernilai"number". - Keluaran
query bukan angka: nilaitigatidak bisa dikonversi menjadi integer, sehingga Fastify menolak permintaan sebelum handler berjalan.
Error Handler dan Plugin #
Untuk error terpusat, Fastify menyediakan setErrorHandler — padanan error middleware empat argumen pada Express. Penangan 404 juga punya tempat khusus lewat setNotFoundHandler, dan kumpulan rute dapat dikelompokkan sebagai plugin dengan register.
import Fastify from 'fastify';
const host = '127.0.0.1';
const app = Fastify();
// Handler error terpusat: didaftarkan sebelum semua rute.
app.setErrorHandler((err, req, reply) => {
const status = err.statusCode ?? 500;
reply.status(status).send({ error: { status, pesan: err.message } });
});
// Handler 404 terpusat.
app.setNotFoundHandler((req, reply) => {
reply.status(404).send({
error: { status: 404, pesan: `Rute ${req.method} ${req.url} tidak ditemukan` },
});
});
// Rute async yang melempar error.
app.get('/gagal', async () => {
throw new Error('Sambungan basis data terputus');
});
// Rute dengan statusCode khusus.
app.get('/dilarang', async () => {
const err = new Error('Token tidak valid');
err.statusCode = 401;
throw err;
});
// Plugin: kumpulan rute dengan prefix sendiri.
await app.register(async (instance) => {
instance.get('/versi', async () => ({ modul: 'info', versi: '1.0.0' }));
}, { prefix: '/info' });
await app.listen({ port: 0, host });
const base = `http://${host}:${app.server.address().port}`;
const r1 = await fetch(`${base}/gagal`);
console.log('gagal :', r1.status, await r1.text());
const r2 = await fetch(`${base}/dilarang`);
console.log('dilarang:', r2.status, await r2.text());
const r3 = await fetch(`${base}/info/versi`);
console.log('plugin :', r3.status, await r3.text());
const r4 = await fetch(`${base}/tak-ada`);
console.log('tak ada :', r4.status, await r4.text());
await app.close();Output yang Dihasilkan:
gagal : 500 {"error":{"status":500,"pesan":"Sambungan basis data terputus"}}
dilarang: 401 {"error":{"status":401,"pesan":"Token tidak valid"}}
plugin : 200 {"modul":"info","versi":"1.0.0"}
tak ada : 404 {"error":{"status":404,"pesan":"Rute GET /tak-ada tidak ditemukan"}}Mari kita bedah komponen-komponennya:
- Baris 7-10 (
setErrorHandler): handler menerima(err, req, reply)dan mengirim respons lewatreply. Error dari handler async diteruskan ke sini secara otomatis — perilaku bawaan Fastify sejak versi awal. Propertyerr.statusCodemengikuti penanda yang kita pasang pada rute/dilarang. - Baris 13-17 (
setNotFoundHandler): satu tempat untuk semua 404, sepadan dengan middleware 404 di akhir stack Express. - Baris 32-34 (
app.register): plugin berupa async function yang menerimainstanceterpisah — rute di dalamnya tidak terlihat dari luar, dan prefix/infohanya berlaku pada kelompok itu. Kata kunciawaitdiperlukan karena pemuatan plugin berjalan asinkron; menunggunya memastikan rute plugin sudah terpasang sebelumlisten.
Fastify dan Express Berdampingan #
| Aspek | Express | Fastify |
|---|---|---|
| Bentuk handler | callback (req, res) yang memanggil res.json | async function yang mengembalikan nilai |
| Validasi input | package terpisah, misalnya middleware validasi | JSON Schema bawaan per rute |
| Penanganan error | error middleware empat argumen | setErrorHandler terpusat |
| Pengelompokan rute | express.Router() | plugin register dengan prefix |
| Logging | package terpisah seperti morgan | opsi logger bawaan berbasis pino |
Ketika sebuah rute juga diberi skema respons, Fastify mengompilasi serializer khusus dari skema tersebut sehingga pengubahan object menjadi JSON berjalan lebih cepat dibanding JSON.stringify umum — salah satu sumber performa tinggi yang sering dikutip dari tolok ukur resmi Fastify. Skema juga memberi efek samping yang berguna: dokumentasi API dapat dihasilkan langsung dari skema yang sama.
Kita kini memiliki dua framework dengan kekuatan berbeda di tangan. Bagaimana memutuskan antara module http polos, Express, Fastify, atau bahkan framework berarsitektur penuh seperti NestJS? Chapter penutup section ini menyusun kerangka berpikirnya.