September 27, 2026

Fastify.

Mengenal Fastify 5: plugin & encapsulation, hooks, validasi berbasis JSON Schema, type provider, testing dengan inject, dan kenapa Fastify cepat.

Fastify adalah framework web Node.js yang fokus pada performa rendah-overhead dan developer experience lewat sistem plugin dan skema. Aku memakai Fastify sebagai backend di Proxmox Management Server (Fastify + TypeScript + Drizzle + BullMQ), jadi catatan ini juga merangkum pola yang terasa berguna di project nyata.

Versi yang dibahas: Fastify 5 (terbaru di npm per September 2026: 5.12.x). Fastify v5 hanya mendukung Node.js 20+.


1. Filosofi

  • Schema-first — route idealnya punya JSON Schema untuk body, querystring, params, dan response. Skema ini dipakai untuk validasi dan serialisasi.
  • Semua adalah plugin — route, database, auth, bahkan konfigurasi didaftarkan sebagai plugin lewat register().
  • Encapsulation — plugin punya “konteks” sendiri. Decorator dan hook yang didaftarkan di dalam plugin tidak bocor ke luar kecuali sengaja dibuka.
  • Overhead sekecil mungkin — logger (Pino), router, dan serializer dipilih/dibuat supaya cepat.
  • Tetap tidak terlalu opinionated soal struktur folder, ORM, atau arsitektur.

2. Struktur Project

Struktur umum dengan @fastify/autoload (atau yang dihasilkan fastify-cli):

fastify-api/
├── src/
│   ├── app.ts              # build app: register plugins & routes
│   ├── server.ts           # listen()
│   ├── plugins/            # plugin global (dibungkus fastify-plugin)
│   │   ├── db.ts           # decorate fastify.db
│   │   ├── auth.ts         # decorate request.user + hook
│   │   └── sensible.ts
│   ├── routes/             # tiap folder = prefix URL (autoload)
│   │   ├── root.ts
│   │   └── users/
│   │       ├── index.ts    # /users
│   │       └── schema.ts
│   ├── services/
│   │   └── users.service.ts
│   └── db/
│       ├── schema.ts       # Drizzle schema
│       └── migrations/
├── test/
│   └── users.test.ts
├── tsconfig.json
└── package.json

Pola app.ts terpisah dari server.ts penting karena testing Fastify memakai app.inject() tanpa membuka port.


3. Routing

Fastify memakai router find-my-way yang berbasis radix tree, jadi pencocokan route tidak bergantung pada jumlah route secara linear.

app.get('/users', async () => users)

app.get<{ Params: { id: string } }>('/users/:id', async (request, reply) => {
  const user = users.find((u) => u.id === Number(request.params.id))
  if (!user) return reply.code(404).send({ message: 'Not found' })
  return user // return value = response body
})

// bentuk lengkap
app.route({
  method: 'POST',
  url: '/users',
  schema: { body: createUserSchema },
  handler: async (request, reply) => { /* ... */ },
})

Ciri khas: handler async cukup me-return nilai; Fastify yang menyerialisasi dan mengirim.

Mengelompokkan route dengan prefix dilakukan lewat plugin:

app.register(usersRoutes, { prefix: '/api/users' })

4. Plugin, Decorator, dan Encapsulation

Plugin adalah fungsi async (fastify, opts) => {}. Di dalamnya kamu bisa mendaftarkan route, hook, decorator, atau plugin lain.

// src/plugins/db.ts
import fp from 'fastify-plugin'
import { drizzle } from 'drizzle-orm/mysql2'

export default fp(async (fastify) => {
  const db = drizzle(process.env.DATABASE_URL!)
  fastify.decorate('db', db)
  fastify.addHook('onClose', async () => {
    // tutup koneksi pool di sini
  })
})

declare module 'fastify' {
  interface FastifyInstance {
    db: ReturnType<typeof drizzle>
  }
}
KonsepPenjelasan
register(plugin)Membuat konteks anak baru
fastify-plugin (fp)“Membuka” encapsulation — decorator/hook terlihat oleh parent
decorate()Menambah properti ke instance (fastify.db)
decorateRequest() / decorateReply()Menambah properti ke request/reply

Encapsulation inilah yang menggantikan DI container di Fastify: dependency (db, config, service) ditempel ke instance lewat decorator, dan cakupannya diatur oleh pohon plugin.


5. Hooks (Lifecycle)

Fastify tidak memakai middleware gaya (req, res, next) sebagai konsep utama; penggantinya adalah hooks di setiap tahap lifecycle.

HookKapan dipanggilContoh penggunaan
onRequestRequest masuk, body belum di-parseAuth token, rate limit
preParsingSebelum body di-parseDekompresi stream
preValidationSebelum validasi skemaNormalisasi input
preHandlerSetelah validasi, sebelum handlerCek otorisasi/role
preSerializationSebelum payload diserialisasiBungkus response
onSendSebelum dikirimUbah header
onResponseResponse sudah terkirimMetrics, logging
onErrorTerjadi errorLaporan error

Hook aplikasi (bukan per request): onReady, onListen, onClose, onRoute, onRegister.

app.addHook('onRequest', async (request, reply) => {
  if (!request.headers.authorization) {
    return reply.code(401).send({ message: 'Unauthorized' })
  }
})

Kalau benar-benar perlu middleware Express, ada @fastify/express / @fastify/middie, tapi sebaiknya pakai plugin native (@fastify/cors, @fastify/helmet, @fastify/rate-limit, dll.).


6. Validasi & Serialisasi

Validasi bawaan Fastify memakai JSON Schema yang dikompilasi oleh Ajv saat startup. Skema response dikompilasi oleh fast-json-stringify.

const userSchema = {
  type: 'object',
  properties: {
    id: { type: 'integer' },
    name: { type: 'string' },
    email: { type: 'string', format: 'email' },
  },
} as const

app.get('/users/:id', {
  schema: {
    params: { type: 'object', properties: { id: { type: 'integer' } }, required: ['id'] },
    response: { 200: userSchema },
  },
}, async (request) => { /* ... */ })

Dua keuntungan skema response:

  1. Lebih cepat — fast-json-stringify membuat fungsi serialisasi khusus dari skema, sehingga tidak perlu menebak tipe setiap properti seperti JSON.stringify.
  2. Lebih aman — properti yang tidak ada di skema (misalnya password) tidak ikut terkirim.

Type Provider (TypeBox)

Supaya skema sekaligus menjadi tipe TypeScript, pakai type provider. Untuk @fastify/type-provider-typebox v6, TypeBox di-install sebagai paket typebox:

npm i typebox @fastify/type-provider-typebox
import Fastify from 'fastify'
import { Type, TypeBoxTypeProvider } from '@fastify/type-provider-typebox'

const app = Fastify().withTypeProvider<TypeBoxTypeProvider>()

app.post('/users', {
  schema: {
    body: Type.Object({
      name: Type.String({ minLength: 1 }),
      email: Type.String({ format: 'email' }),
    }),
  },
}, async (request) => {
  request.body.email // bertipe string, tanpa deklarasi manual
})

Alternatif: fastify-type-provider-zod jika tim lebih suka Zod.


7. Data Layer / ORM

Fastify tidak mengharuskan ORM tertentu. Polanya: buat plugin yang men-decorate instance dengan client database.

PilihanCatatan
DrizzleSkema TypeScript, migrasi ter-typing — dipakai di Proxmox Management Server
PrismaBuat plugin yang decorate fastify.prisma
@fastify/mysql, @fastify/postgresPlugin resmi berbasis driver langsung
Kysely / KnexQuery builder

Tutup koneksi di hook onClose supaya app.close() (termasuk di test) bersih.


8. Testing

Fastify punya app.inject() (berbasis light-my-request) yang menyuntikkan request palsu langsung ke router, tanpa socket HTTP. Cepat dan tidak butuh port.

// test/users.test.ts
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { buildApp } from '../src/app.js'

test('GET /users', async (t) => {
  const app = buildApp()
  t.after(() => app.close())

  const res = await app.inject({ method: 'GET', url: '/users' })
  assert.equal(res.statusCode, 200)
  assert.ok(Array.isArray(res.json()))
})

test('POST /users menolak email invalid', async (t) => {
  const app = buildApp()
  t.after(() => app.close())

  const res = await app.inject({
    method: 'POST',
    url: '/users',
    payload: { name: 'A', email: 'bukan-email' },
  })
  assert.equal(res.statusCode, 400)
})

Catatan: error validasi bawaan Fastify mengembalikan 400, bukan 422.


9. Performa — Kenapa Fastify Cepat?

Angka dari benchmark resmi fastify/benchmarks (juga ditampilkan di fastify.dev/benchmarks ), run 2 September 2026, Node v24.20.0, mesin Linux x64 4 vCPU, autocannon -c 100 -d 40 -p 10, endpoint “hello world” JSON:

FrameworkVersiReq/sLatency (ms)
node-http (tanpa framework)v24.20.098.8649,61
fastify5.12.197.5959,73
adonisjs (http-server)9.3.089.83710,67
hono4.13.588.52510,80
koa3.2.178.66212,21
express5.2.159.65116,25

Artinya overhead Fastify di atas http bawaan Node sangat tipis. Sumber kecepatannya:

  1. find-my-way — router radix tree, lookup cepat walau route banyak.
  2. fast-json-stringify — serializer hasil kompilasi dari skema response.
  3. Validasi terkompilasi — skema Ajv dikompilasi sekali saat startup, bukan diinterpretasi per request.
  4. Pino — logger JSON dengan overhead rendah.
  5. Hook tanpa rantai middleware panjang dan objek request/reply yang ringan.

Tetap ingat: ini benchmark endpoint kosong. Tim Fastify sendiri menyebut angka ini ilustratif dan menyarankan mengukur dengan beban kerja sendiri. Kalau handler-mu menunggu database 20 ms, selisih framework jadi kecil. Ada juga baris fastify-big-json (~19.929 req/s) yang menunjukkan ukuran payload JSON sangat memengaruhi hasil.


10. Ekosistem & Kematangan

AspekPenilaian
UmurSejak 2016, v5 dirilis 2024
GovernanceProyek OpenJS Foundation
Plugin resmi@fastify/*: cors, helmet, jwt, cookie, multipart, rate-limit, swagger, websocket, static, autoload
TypeScriptTipe bawaan + type provider
Dokumentasifastify.dev — lengkap, termasuk LTS policy
Dipakai sebagai adapterNestJS (@nestjs/platform-fastify)

11. Contoh Minimal yang Bisa Dijalankan

mkdir fastify-demo && cd fastify-demo
npm init -y
npm pkg set type=module
npm install fastify

server.js:

import Fastify from 'fastify'

const app = Fastify({ logger: true })
const users = [{ id: 1, name: 'Fanny', email: '[email protected]', password: 'rahasia' }]

const userSchema = {
  type: 'object',
  properties: {
    id: { type: 'integer' },
    name: { type: 'string' },
    email: { type: 'string' },
  },
}

app.get('/users', {
  schema: { response: { 200: { type: 'array', items: userSchema } } },
}, async () => users) // password otomatis tidak ikut terkirim

app.post('/users', {
  schema: {
    body: {
      type: 'object',
      required: ['name', 'email'],
      properties: {
        name: { type: 'string', minLength: 1 },
        email: { type: 'string', format: 'email' },
      },
    },
    response: { 201: userSchema },
  },
}, async (request, reply) => {
  const user = { id: users.length + 1, ...request.body }
  users.push(user)
  return reply.code(201).send(user)
})

await app.listen({ port: 3000 })
node server.js

curl localhost:3000/users
curl -X POST localhost:3000/users \
  -H 'Content-Type: application/json' \
  -d '{"name":"Budi","email":"[email protected]"}'
# body invalid → 400 dengan pesan dari Ajv

12. Kapan Memilih Fastify?

  • API/backend yang throughput dan latency-nya penting.
  • Suka pendekatan schema-first (validasi + dokumentasi OpenAPI via @fastify/swagger).
  • Ingin struktur modular (plugin) tanpa “berat”-nya framework penuh seperti NestJS.

Lanjut ke NestJS yang bisa berjalan di atas Fastify, atau lihat Perbandingan Framework Node.js .


Referensi

Hey! I’m Fanny, the software engineer tending to this digital garden. You can read more about me, or subscribe by email.

Comments