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.

js
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:

text
GET /     : 200 {"pesan":"Selamat datang di Fastify"}
GET /halo : 200 {"pesan":"Halo, Siti"}

Empat perbedaan dengan Express terlihat jelas pada berkas pendek ini:

  1. 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.
  2. Baris 10 (await app.listen(...)): listen mengembalikan promise, sehingga proses menunggu server benar-benar siap sebelum baris berikutnya dijalankan. Kita tidak perlu callback seperti pada Express.
  3. Baris 11 (app.server.address().port): instance Fastify membungkus server HTTP asli di properti app.server, tempat kita membaca port ephemeral untuk pengujian.
  4. 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.

js
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:

text
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:

  1. Baris 6-17 (skemaBuat): skema body menyatakan judul wajib berupa string minimal tiga karakter dan selesai opsional bertipe boolean. Keluaran judul pendek dan tanpa judul menunjukkan dua pelanggaran berbeda — semuanya ditolak 400 dengan kode FST_ERR_VALIDATION tanpa satu baris kode validasi pun di handler.
  2. Baris 23-29 (skema querystring): skema query menyatakan halaman bertipe integer. Keluaran query integer memperlihatkan nilai "3" pada URL berubah menjadi angka 3 — itulah mengapa jenis bernilai "number".
  3. Keluaran query bukan angka: nilai tiga tidak 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.

js
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:

text
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:

  1. Baris 7-10 (setErrorHandler): handler menerima (err, req, reply) dan mengirim respons lewat reply. Error dari handler async diteruskan ke sini secara otomatis — perilaku bawaan Fastify sejak versi awal. Property err.statusCode mengikuti penanda yang kita pasang pada rute /dilarang.
  2. Baris 13-17 (setNotFoundHandler): satu tempat untuk semua 404, sepadan dengan middleware 404 di akhir stack Express.
  3. Baris 32-34 (app.register): plugin berupa async function yang menerima instance terpisah — rute di dalamnya tidak terlihat dari luar, dan prefix /info hanya berlaku pada kelompok itu. Kata kunci await diperlukan karena pemuatan plugin berjalan asinkron; menunggunya memastikan rute plugin sudah terpasang sebelum listen.

Fastify dan Express Berdampingan #

AspekExpressFastify
Bentuk handlercallback (req, res) yang memanggil res.jsonasync function yang mengembalikan nilai
Validasi inputpackage terpisah, misalnya middleware validasiJSON Schema bawaan per rute
Penanganan errorerror middleware empat argumensetErrorHandler terpusat
Pengelompokan ruteexpress.Router()plugin register dengan prefix
Loggingpackage terpisah seperti morganopsi 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.

Referensi #