Fastify plugin pattern dengan TypeScript generics
Bikin Fastify plugin yang fully type-safe — decorator, hooks, request properties terdeteksi TS dari plugin lain. Pattern decorate + augment.
Dipublikasikan 26 Juni 2026
Fastify plugin ekosistem-nya rapi tapi pattern typing-nya sering bingungin. Decorator yang di-attach di plugin A harus terdeteksi di plugin B. Snippet ini contoh plugin db dan auth dengan TypeScript module augmentation supaya request.user dan app.db auto-complete di route handler.
Kode
// src/plugins/db.ts
import fp from "fastify-plugin";
import { Pool } from "pg";
import type { FastifyPluginAsync } from "fastify";
declare module "fastify" {
interface FastifyInstance {
db: Pool;
}
}
interface DbPluginOptions {
connectionString: string;
maxConnections?: number;
}
const dbPlugin: FastifyPluginAsync<DbPluginOptions> = async (app, opts) => {
const pool = new Pool({
connectionString: opts.connectionString,
max: opts.maxConnections ?? 10,
});
// Smoke test koneksi saat boot
await pool.query("SELECT 1");
app.log.info("DB pool ready");
app.decorate("db", pool);
app.addHook("onClose", async () => {
app.log.info("Closing DB pool");
await pool.end();
});
};
export default fp(dbPlugin, {
name: "db",
fastify: "5.x",
});
// src/plugins/auth.ts
import fp from "fastify-plugin";
import jwt from "jsonwebtoken";
import type { FastifyPluginAsync, FastifyRequest } from "fastify";
interface AuthUser {
id: number;
email: string;
role: "admin" | "user" | "staff";
}
declare module "fastify" {
interface FastifyRequest {
user?: AuthUser;
}
interface FastifyInstance {
authenticate: (req: FastifyRequest) => Promise<AuthUser>;
requireRole: (role: AuthUser["role"]) => (req: FastifyRequest) => Promise<void>;
}
}
interface AuthPluginOptions {
secret: string;
}
const authPlugin: FastifyPluginAsync<AuthPluginOptions> = async (app, opts) => {
// Helper untuk decode + verifikasi
async function authenticate(req: FastifyRequest): Promise<AuthUser> {
const header = req.headers.authorization;
if (!header?.startsWith("Bearer ")) {
throw app.httpErrors.unauthorized("Token tidak ada");
}
const token = header.slice(7);
try {
const payload = jwt.verify(token, opts.secret) as AuthUser;
// Optional: re-fetch user dari DB untuk role check terbaru
const result = await app.db.query<AuthUser>(
"SELECT id, email, role FROM users WHERE id = $1",
[payload.id]
);
if (result.rows.length === 0) {
throw app.httpErrors.unauthorized("User tidak ditemukan");
}
const user = result.rows[0];
req.user = user;
return user;
} catch (err) {
if (err instanceof jwt.JsonWebTokenError) {
throw app.httpErrors.unauthorized("Token tidak valid");
}
throw err;
}
}
function requireRole(role: AuthUser["role"]) {
return async (req: FastifyRequest) => {
const user = await authenticate(req);
if (user.role !== role && user.role !== "admin") {
throw app.httpErrors.forbidden(`Butuh role ${role}`);
}
};
}
app.decorate("authenticate", authenticate);
app.decorate("requireRole", requireRole);
};
// dependencies: pastikan db plugin load duluan
export default fp(authPlugin, {
name: "auth",
dependencies: ["db"],
});
// src/server.ts
import Fastify from "fastify";
import sensible from "@fastify/sensible";
import dbPlugin from "./plugins/db";
import authPlugin from "./plugins/auth";
const app = Fastify({ logger: true });
await app.register(sensible); // app.httpErrors.*
await app.register(dbPlugin, {
connectionString: process.env.DATABASE_URL!,
});
await app.register(authPlugin, {
secret: process.env.JWT_SECRET!,
});
// Route dengan type-safe access ke db + authenticate
app.get(
"/api/me",
{ preHandler: app.authenticate },
async (req) => {
// req.user fully typed sebagai AuthUser (bukan undefined)
return { id: req.user!.id, email: req.user!.email };
}
);
app.get(
"/api/admin/produk",
{ preHandler: app.requireRole("admin") },
async (_req) => {
const result = await app.db.query<{ id: number; nama: string }>(
"SELECT id, nama FROM produk ORDER BY id DESC LIMIT 20"
);
return result.rows;
}
);
await app.listen({ port: 3000, host: "0.0.0.0" });
Pemakaian
// Test plugin via inject (tanpa start server)
import { test } from "node:test";
import { strict as assert } from "node:assert";
test("authenticated route butuh token", async () => {
const response = await app.inject({
method: "GET",
url: "/api/me",
});
assert.equal(response.statusCode, 401);
});
test("authenticated route dengan token valid", async () => {
const token = jwt.sign({ id: 1, email: "asti@id", role: "user" }, secret);
const response = await app.inject({
method: "GET",
url: "/api/me",
headers: { authorization: `Bearer ${token}` },
});
assert.equal(response.statusCode, 200);
});
// Plugin tambahan reuse plugin yang sudah ada
const orderPlugin: FastifyPluginAsync = async (app) => {
app.post(
"/api/order",
{ preHandler: app.authenticate },
async (req, reply) => {
const { produk_id, qty } = req.body as { produk_id: number; qty: number };
const result = await app.db.query<{ id: number }>(
"INSERT INTO orders (user_id, produk_id, qty) VALUES ($1,$2,$3) RETURNING id",
[req.user!.id, produk_id, qty]
);
return reply.code(201).send({ order_id: result.rows[0].id });
}
);
};
Kapan dipakai
- Backend API dengan banyak cross-cutting concern (auth, DB, logging, metrics).
- Microservices yang share plugin via internal npm package.
- App dengan multiple environment (test/dev/prod) yang butuh DI clean.
- Project yang growth-stage — modular plugin tanggal jadi clean architecture.
Catatan
fp()wrapper wajib supaya plugin tidak encapsulated. Tanpa fp, decorator hanya accessible di plugin tree dalam saja.- declare module “fastify” — module augmentation harus di file yang di-import oleh tsc. Kalau pakai esbuild dengan tree-shake aggresif, pastikan plugin file tidak di-shake.
- dependencies: [“db”] — Fastify enforce order plugin. Kalau auth load duluan tanpa dependency,
app.dbundefined. - Decorator name conflict —
app.decorate("user", ...)clash denganrequest.user. Pakai prefix konsisten kalau perlu. - Async plugin — semua plugin pakai
async+await app.register. Tanpa await, race condition saat startup.
Jangan abuse decorator untuk passing data per-request. Pakai
request.decorateProperty(singular instance) atau context object. Decorate instance untuk shared resource (DB pool, logger).
# tags
fastifyplugintypescriptdecoratornode
Ditulis oleh Asti Larasati · 26 Juni 2026