Backend Development

Fastify: Fast Node.js Framework

Tutorial lengkap belajar Fastify dari nol β€” plugins, hooks, validation, TypeScript integration, dan membangun HTTP API yang ultra-cepat dan scalable

1. Pengenalan Fastify

Fastify adalah web framework yang sangat cepat dan berbiaya rendah untuk Node.js, dikembangkan oleh Matteo Collina dan Tomas Della Vedova. Fastify dirancang untuk memberikan performa terbaik dengan overhead yang minimal β€” menjadikannya salah satu framework HTTP tercepat yang tersedia untuk Node.js.

Fastify menggunakan JSON Schema untuk validasi dan serialisasi data, yang tidak hanya memastikan keamanan input, tetapi juga mempercepat serialisasi JSON hingga 2-3x lebih cepat dibandingkan JSON.stringify(). Arsitektur plugin-nya membuat kode sangat modular dan mudah di-maintain.

Mengapa Memilih Fastify?

Keunggulan Penjelasan
Performa TinggiHingga 30,000+ requests/detik β€” jauh lebih cepat dari Express (~7,000 req/s)
JSON Schema ValidationValidasi input built-in dengan JSON Schema yang juga mempercepat serialisasi
Plugin ArchitectureSistem plugin yang sangat modular untuk encapsulation dan reuse
TypeScript SupportTypeScript support bawaan dengan type inference yang sangat baik
Lifecycle Hooks10 hooks yang mencakup seluruh lifecycle request
Logging Built-inPino logger terintegrasi β€” logging performant dengan zero-cost abstraction

Fastify vs Express.js

Aspek Fastify Express.js
Performa~30,000 req/s~7,000 req/s
ValidasiJSON Schema built-inManual / express-validator
Serialisasifast-json-stringify (2-3x lebih cepat)JSON.stringify()
LoggingPino (built-in)Manual / morgan / winston
ArchitecturePlugin-based (encapsulated)Middleware-based
TypeScriptFirst-class supportPerlu @types/express
Learning Curve🟑 Sedang🟒 Mudah
Ekosistem🟑 Berkembang pesat🟒 Sangat besar
Diagram: Request Lifecycle di Fastify
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              FASTIFY REQUEST LIFECYCLE                   β”‚
β”‚                                                         β”‚
β”‚  Incoming Request                                       β”‚
β”‚         β”‚                                               β”‚
β”‚         β–Ό                                               β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                                       β”‚
β”‚  β”‚  onRequest   β”‚  ← Hook: Logging, CORS, timing       β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜                                       β”‚
β”‚         β–Ό                                               β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                                       β”‚
β”‚  β”‚  preParsing  β”‚  ← Hook: Modify raw body             β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜                                       β”‚
β”‚         β–Ό                                               β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                                       β”‚
β”‚  β”‚  preParsing  β”‚  ← Parse request body                β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜                                       β”‚
β”‚         β–Ό                                               β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                                       β”‚
β”‚  β”‚ preValidationβ”‚  ← Hook: Before schema validation    β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜                                       β”‚
β”‚         β–Ό                                               β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                                       β”‚
β”‚  β”‚  Validation  β”‚  ← JSON Schema validation            β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜                                       β”‚
β”‚         β–Ό                                               β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                                       β”‚
β”‚  β”‚ preHandler   β”‚  ← Hook: Auth check, rate limiting   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜                                       β”‚
β”‚         β–Ό                                               β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                                       β”‚
β”‚  β”‚   Handler    β”‚  ← Route handler (business logic)    β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜                                       β”‚
β”‚         β–Ό                                               β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                                       β”‚
β”‚  β”‚onSend/       β”‚  ← Hook: Modify response, logging    β”‚
β”‚  β”‚onResponse    β”‚                                       β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                       β”‚
β”‚         β–Ό                                               β”‚
β”‚  Response Sent                                          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

2. Setup dan Instalasi

Instalasi Dasar

bash
# Buat proyek baru
mkdir my-fastify-api
cd my-fastify-api
npm init -y

# Instal Fastify
npm install fastify

# Untuk TypeScript (opsional tapi direkomendasikan)
npm install -D typescript @types/node tsx

# Buat file konfigurasi TypeScript
npx tsc --init

Hello World

typescript
// src/server.ts
import Fastify from 'fastify';

// Buat instance Fastify
const fastify = Fastify({
  logger: true  // Aktifkan Pino logger
});

// Definisikan route
fastify.get('/', async (request, reply) => {
  return { message: 'Halo dari Fastify!' };
});

// Jalankan server
const start = async () => {
  try {
    await fastify.listen({ port: 3000, host: '0.0.0.0' });
    console.log('Server berjalan di http://localhost:3000');
  } catch (err) {
    fastify.log.error(err);
    process.exit(1);
  }
};

start();
bash
# Jalankan dengan tsx (TypeScript execution)
npx tsx src/server.ts

# Atau dengan npm script
# package.json:
# "scripts": {
#   "dev": "tsx watch src/server.ts",
#   "build": "tsc",
#   "start": "node dist/server.js"
# }

Struktur Proyek yang Direkomendasikan

text
my-fastify-api/
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ plugins/           # Fastify plugins
β”‚   β”‚   β”œβ”€β”€ db.ts          # Database plugin
β”‚   β”‚   β”œβ”€β”€ auth.ts        # Auth plugin
β”‚   β”‚   └── swagger.ts     # Swagger plugin
β”‚   β”œβ”€β”€ routes/            # Route definitions
β”‚   β”‚   β”œβ”€β”€ products/
β”‚   β”‚   β”‚   β”œβ”€β”€ index.ts   # /products routes
β”‚   β”‚   β”‚   β”œβ”€β”€ schemas.ts # JSON schemas
β”‚   β”‚   β”‚   └── handlers.ts
β”‚   β”‚   └── users/
β”‚   β”‚       β”œβ”€β”€ index.ts
β”‚   β”‚       β”œβ”€β”€ schemas.ts
β”‚   β”‚       └── handlers.ts
β”‚   β”œβ”€β”€ types/             # TypeScript types
β”‚   β”‚   └── fastify.d.ts   # Fastify type augmentations
β”‚   β”œβ”€β”€ utils/             # Utility functions
β”‚   β”œβ”€β”€ app.ts             # App setup (plugins, routes)
β”‚   └── server.ts          # Entry point
β”œβ”€β”€ tests/                 # Tests
β”œβ”€β”€ .env                   # Environment variables
β”œβ”€β”€ tsconfig.json
β”œβ”€β”€ package.json
└── README.md

3. Routing di Fastify

Fastify mendukung beberapa cara untuk mendaftarkan routes: inline di instance, menggunakan fastify.register() dengan plugin, atau menggunakan @fastify/routes.

Dasar Routing

typescript
import Fastify from 'fastify';

const fastify = Fastify({ logger: true });

// === HTTP Methods ===
fastify.get('/products', async (request, reply) => {
  return { products: [] };
});

fastify.get('/products/:id', async (request, reply) => {
  const { id } = request.params as { id: string };
  return { id, name: 'Produk #' + id };
});

fastify.post('/products', async (request, reply) => {
  const body = request.body as any;
  reply.code(201); // Status 201 Created
  return { message: 'Produk dibuat', product: body };
});

fastify.put('/products/:id', async (request, reply) => {
  const { id } = request.params as { id: string };
  return { message: `Produk ${id} diupdate` };
});

fastify.delete('/products/:id', async (request, reply) => {
  const { id } = request.params as { id: string };
  reply.code(204); // No Content
  return;
});

// === Prefix Routes ===
fastify.register(async function apiRoutes(app) {
  app.get('/users', async () => ({ users: [] }));
  app.get('/users/:id', async (req) => {
    const { id } = req.params as { id: string };
    return { id, name: 'User #' + id };
  });
  app.post('/users', async (req, reply) => {
    reply.code(201);
    return { message: 'User dibuat' };
  });
}, { prefix: '/api/v1' });
// Hasil: /api/v1/users, /api/v1/users/:id

Route Options dan Config

typescript
// Route dengan full options
fastify.route({
  method: 'GET',
  url: '/products',
  schema: {
    querystring: {
      type: 'object',
      properties: {
        page: { type: 'integer', default: 1 },
        limit: { type: 'integer', default: 20 },
        category: { type: 'string' }
      }
    },
    response: {
      200: {
        type: 'object',
        properties: {
          products: {
            type: 'array',
            items: {
              type: 'object',
              properties: {
                id: { type: 'integer' },
                name: { type: 'string' },
                price: { type: 'number' }
              }
            }
          },
          total: { type: 'integer' }
        }
      }
    }
  },
  handler: async (request, reply) => {
    const { page, limit, category } = request.query as any;
    // ... fetch products
    return { products: [], total: 0 };
  }
});

// Shorthand dengan schema
fastify.post('/products', {
  schema: {
    body: {
      type: 'object',
      required: ['name', 'price'],
      properties: {
        name: { type: 'string', minLength: 1 },
        price: { type: 'number', minimum: 0 },
        description: { type: 'string' }
      }
    }
  }
}, async (request, reply) => {
  // request.body sudah tervalidasi dan bertipe!
  const { name, price, description } = request.body as any;
  return { name, price, description };
});

4. Sistem Plugin

Plugin adalah cara utama untuk memperluas fungsionalitas Fastify. Setiap plugin memiliki encapsulation scope sendiri β€” artinya route, hook, dan dekorator yang didaftarkan di dalam plugin tidak mempengaruhi luar plugin tersebut.

Membuat Plugin

typescript
// plugins/db.ts β€” Database plugin
import fp from 'fastify-plugin';
import { PrismaClient } from '@prisma/client';

// Deklarasi type augmentation
declare module 'fastify' {
  interface FastifyInstance {
    db: PrismaClient;
  }
}

async function dbPlugin(fastify: FastifyInstance) {
  const prisma = new PrismaClient();

  // Dekorasi instance dengan Prisma
  fastify.decorate('db', prisma);

  // Cleanup saat server ditutup
  fastify.addHook('onClose', async () => {
    await prisma.$disconnect();
    fastify.log.info('Database connection closed');
  });
}

// fp() memastikan plugin hanya di-load sekali
export default fp(dbPlugin, {
  name: 'db'
});

// === routes/products.ts β€” Product routes plugin ===
import { FastifyInstance } from 'fastify';

export default async function productRoutes(fastify: FastifyInstance) {
  // GET /products β€” Ambil semua produk
  fastify.get('/', async (request, reply) => {
    const products = await fastify.db.product.findMany({
      orderBy: { createdAt: 'desc' }
    });
    return { products };
  });

  // GET /products/:id β€” Ambil produk by ID
  fastify.get('/:id', async (request, reply) => {
    const { id } = request.params as { id: string };
    const product = await fastify.db.product.findUnique({
      where: { id: parseInt(id) }
    });

    if (!product) {
      reply.code(404);
      return { error: 'Produk tidak ditemukan' };
    }

    return { product };
  });

  // POST /products β€” Buat produk baru
  fastify.post('/', async (request, reply) => {
    const body = request.body as any;
    const product = await fastify.db.product.create({
      data: body
    });
    reply.code(201);
    return { product };
  });
}

Mendaftarkan Plugin

typescript
// app.ts β€” App setup
import Fastify from 'fastify';
import dbPlugin from './plugins/db';
import productRoutes from './routes/products';
import userRoutes from './routes/users';

export function buildApp() {
  const fastify = Fastify({
    logger: {
      level: 'info',
      transport: {
        target: 'pino-pretty',  // Format log yang indah
        options: { colorize: true }
      }
    }
  });

  // Register plugins
  fastify.register(dbPlugin);

  // Register routes dengan prefix
  fastify.register(productRoutes, { prefix: '/api/products' });
  fastify.register(userRoutes, { prefix: '/api/users' });

  // Health check
  fastify.get('/health', async () => {
    return { status: 'ok', timestamp: new Date().toISOString() };
  });

  return fastify;
}

// server.ts β€” Entry point
import { buildApp } from './app';

const app = buildApp();

app.listen({ port: 3000, host: '0.0.0.0' }, (err) => {
  if (err) {
    app.log.error(err);
    process.exit(1);
  }
});

5. Lifecycle Hooks

Fastify menyediakan 10 lifecycle hooks yang memungkinkan kamu menjalankan kode pada titik-titik spesifik dalam request lifecycle. Ini sangat berguna untuk logging, autentikasi, dan manipulasi request/response.

typescript
import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';

export default async function authHooks(fastify: FastifyInstance) {

  // === onRequest: Paling awal, sebelum parsing body ===
  fastify.addHook('onRequest', async (request, reply) => {
    request.startTime = Date.now();
    request.log.info({
      method: request.method,
      url: request.url
    }, 'Incoming request');
  });

  // === preHandler: Setelah validasi, sebelum handler ===
  fastify.addHook('preHandler', async (request, reply) => {
    // Cek autentikasi untuk protected routes
    const publicRoutes = ['/api/auth/login', '/api/auth/register', '/health'];
    if (publicRoutes.includes(request.url)) return;

    try {
      const token = request.headers.authorization?.replace('Bearer ', '');
      if (!token) {
        reply.code(401).send({ error: 'Token tidak ditemukan' });
        return;
      }

      const decoded = fastify.jwt.verify(token);
      (request as any).user = decoded;
    } catch (err) {
      reply.code(401).send({ error: 'Token tidak valid' });
    }
  });

  // === onSend: Saat response akan dikirim ===
  fastify.addHook('onSend', async (request, reply, payload) => {
    // Tambahkan response time header
    const duration = Date.now() - (request.startTime || Date.now());
    reply.header('X-Response-Time', `${duration}ms`);

    return payload;
  });

  // === onResponse: Setelah response terkirim ===
  fastify.addHook('onResponse', async (request, reply) => {
    request.log.info({
      statusCode: reply.statusCode,
      duration: `${Date.now() - (request.startTime || Date.now())}ms`
    }, 'Request completed');
  });

  // === onError: Saat terjadi error ===
  fastify.addHook('onError', async (request, reply, error) => {
    request.log.error({
      err: error,
      statusCode: error.statusCode
    }, 'Request error');
  });

  // === Route-level hooks ===
  fastify.get('/admin/dashboard', {
    preHandler: [async (request, reply) => {
      const user = (request as any).user;
      if (user?.role !== 'admin') {
        reply.code(403).send({ error: 'Akses ditolak' });
      }
    }]
  }, async (request) => {
    return { message: 'Admin Dashboard' };
  });
}

6. Validasi dengan JSON Schema

Salah satu fitur paling powerful dari Fastify adalah validasi dan serialisasi berbasis JSON Schema. Saat kamu mendefinisikan schema untuk request dan response, Fastify secara otomatis:

typescript
// schemas/productSchemas.ts
export const createProductSchema = {
  body: {
    type: 'object',
    required: ['name', 'price', 'categoryId'],
    properties: {
      name: {
        type: 'string',
        minLength: 2,
        maxLength: 200,
        errorMessage: {
          minLength: 'Nama produk minimal 2 karakter',
          maxLength: 'Nama produk maksimal 200 karakter'
        }
      },
      description: { type: 'string', maxLength: 2000 },
      price: {
        type: 'number',
        minimum: 0,
        exclusiveMinimum: 0,
        errorMessage: {
          minimum: 'Harga harus lebih dari 0'
        }
      },
      stock: {
        type: 'integer',
        minimum: 0,
        default: 0
      },
      categoryId: { type: 'integer' },
      tags: {
        type: 'array',
        items: { type: 'string' },
        maxItems: 10
      },
      isActive: { type: 'boolean', default: true }
    },
    additionalProperties: false
  },
  response: {
    201: {
      type: 'object',
      properties: {
        id: { type: 'integer' },
        name: { type: 'string' },
        price: { type: 'number' },
        stock: { type: 'integer' },
        isActive: { type: 'boolean' },
        createdAt: { type: 'string', format: 'date-time' }
      }
    }
  }
};

export const getProductsSchema = {
  querystring: {
    type: 'object',
    properties: {
      page: { type: 'integer', default: 1, minimum: 1 },
      limit: { type: 'integer', default: 20, minimum: 1, maximum: 100 },
      category: { type: 'string' },
      search: { type: 'string' },
      sortBy: {
        type: 'string',
        enum: ['name', 'price', 'createdAt', 'stock']
      },
      order: {
        type: 'string',
        enum: ['asc', 'desc'],
        default: 'desc'
      }
    }
  },
  response: {
    200: {
      type: 'object',
      properties: {
        products: {
          type: 'array',
          items: {
            type: 'object',
            properties: {
              id: { type: 'integer' },
              name: { type: 'string' },
              price: { type: 'number' },
              category: { type: 'string' }
            }
          }
        },
        pagination: {
          type: 'object',
          properties: {
            page: { type: 'integer' },
            limit: { type: 'integer' },
            total: { type: 'integer' },
            totalPages: { type: 'integer' }
          }
        }
      }
    }
  }
};

export const getProductByIdSchema = {
  params: {
    type: 'object',
    required: ['id'],
    properties: {
      id: { type: 'integer', minimum: 1 }
    }
  },
  response: {
    200: {
      type: 'object',
      properties: {
        id: { type: 'integer' },
        name: { type: 'string' },
        price: { type: 'number' },
        description: { type: 'string' },
        stock: { type: 'integer' },
        category: {
          type: 'object',
          properties: {
            id: { type: 'integer' },
            name: { type: 'string' }
          }
        },
        reviews: {
          type: 'array',
          items: {
            type: 'object',
            properties: {
              id: { type: 'integer' },
              rating: { type: 'integer' },
              comment: { type: 'string' },
              author: { type: 'string' }
            }
          }
        }
      }
    }
  }
};

// === routes/products.ts β€” Menggunakan schema ===
import { createProductSchema, getProductsSchema, getProductByIdSchema } from '../schemas/productSchemas';

export default async function productRoutes(fastify: FastifyInstance) {
  // Dengan schema validation
  fastify.get('/', { schema: getProductsSchema }, async (request) => {
    const { page, limit, category, search, sortBy, order } =
      request.query as any;
    // Data sudah tervalidasi dan bertipe!
    const products = await fastify.db.product.findMany({
      skip: (page - 1) * limit,
      take: limit,
      orderBy: { [sortBy]: order }
    });
    const total = await fastify.db.product.count();
    return {
      products,
      pagination: { page, limit, total, totalPages: Math.ceil(total / limit) }
    };
  });

  fastify.post('/', { schema: createProductSchema }, async (request, reply) => {
    // request.body sudah tervalidasi β€” aman digunakan!
    const body = request.body as any;
    const product = await fastify.db.product.create({ data: body });
    reply.code(201);
    return product;
  });

  fastify.get('/:id', { schema: getProductByIdSchema }, async (request) => {
    const { id } = request.params as any;
    // ... fetch product
  });
}
πŸ’‘ Mengapa JSON Schema Penting?

JSON Schema di Fastify bukan hanya untuk validasi β€” ia juga digunakan untuk serialisasi response. Fastify menggunakan fast-json-stringify yang menghasilkan fungsi serialisasi khusus untuk setiap schema. Hasilnya: serialisasi JSON yang 2-3x lebih cepat dari JSON.stringify() bawaan Node.js. Ini sangat signifikan untuk high-throughput API.

7. Error Handling

Fastify memiliki sistem error handling yang robust dengan setErrorHandler dan FastifyError yang mendukung HTTP status codes.

typescript
import Fastify, { FastifyError } from 'fastify';

const fastify = Fastify({ logger: true });

// === Global Error Handler ===
fastify.setErrorHandler((error: FastifyError, request, reply) => {
  // Validation error dari JSON Schema
  if (error.validation) {
    request.log.warn({ err: error }, 'Validation error');
    return reply.status(400).send({
      error: 'Validasi Gagal',
      message: error.message,
      details: error.validation
    });
  }

  // HTTP errors
  if (error.statusCode) {
    return reply.status(error.statusCode).send({
      error: error.name,
      message: error.message
    });
  }

  // Unexpected errors β€” jangan expose internal details
  request.log.error({ err: error }, 'Unexpected error');
  return reply.status(500).send({
    error: 'Internal Server Error',
    message: 'Terjadi kesalahan pada server'
  });
});

// === 404 Handler ===
fastify.setNotFoundHandler((request, reply) => {
  reply.status(404).send({
    error: 'Not Found',
    message: `Route ${request.method} ${request.url} tidak ditemukan`
  });
});

// === Custom Error Classes ===
class AppError extends Error {
  statusCode: number;

  constructor(message: string, statusCode: number = 500) {
    super(message);
    this.statusCode = statusCode;
    this.name = 'AppError';
  }
}

class NotFoundError extends AppError {
  constructor(resource: string) {
    super(`${resource} tidak ditemukan`, 404);
    this.name = 'NotFoundError';
  }
}

class ValidationError extends AppError {
  details: any;

  constructor(message: string, details?: any) {
    super(message, 400);
    this.name = 'ValidationError';
    this.details = details;
  }
}

// === Penggunaan di handler ===
fastify.get('/products/:id', async (request, reply) => {
  const { id } = request.params as any;
  const product = await fastify.db.product.findUnique({
    where: { id: parseInt(id) }
  });

  if (!product) {
    throw new NotFoundError('Produk');
  }

  return { product };
});

fastify.post('/products', async (request, reply) => {
  const body = request.body as any;

  if (body.price < 0) {
    throw new ValidationError('Harga tidak boleh negatif', {
      field: 'price',
      value: body.price
    });
  }

  // ... create product
});

8. TypeScript Integration

Fastify memiliki TypeScript support first-class. Kamu bisa mendapatkan full type inference untuk routes, plugins, request body, dan decorators.

typescript
// types/fastify.d.ts β€” Type augmentations
import 'fastify';

declare module 'fastify' {
  // Augment FastifyInstance (decorate)
  interface FastifyInstance {
    db: PrismaClient;
    authenticate: (request: FastifyRequest) => Promise<void>;
  }

  // Augment FastifyRequest
  interface FastifyRequest {
    user?: {
      id: number;
      email: string;
      role: string;
    };
    startTime?: number;
  }
}

// === Typed Route Handler ===
import { FastifyPluginAsyncTypebox, Type } from '@fastify/type-provider-typebox';

const productRoutes: FastifyPluginAsyncTypebox = async (fastify) => {
  fastify.get('/', {
    schema: {
      querystring: Type.Object({
        page: Type.Optional(Type.Integer({ minimum: 1, default: 1 })),
        limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 100, default: 20 })),
      }),
      response: {
        200: Type.Object({
          products: Type.Array(
            Type.Object({
              id: Type.Integer(),
              name: Type.String(),
              price: Type.Number(),
            })
          ),
          total: Type.Integer(),
        }),
      },
    },
  }, async (request) => {
    // request.query sudah bertipe: { page: number, limit: number }
    const { page, limit } = request.query;
    // ... fetch products
    return { products: [], total: 0 };
  });
};

export default productRoutes;

9. Database Integration

Fastify sangat mudah diintegrasikan dengan berbagai database. Berikut contoh menggunakan Prisma ORM dan fastify-postgres.

typescript
// plugins/db-prisma.ts β€” Prisma Plugin
import fp from 'fastify-plugin';
import { PrismaClient } from '@prisma/client';
import { FastifyInstance } from 'fastify';

async function prismaPlugin(fastify: FastifyInstance) {
  const prisma = new PrismaClient({
    log: [
      { level: 'query', emit: 'event' },
      { level: 'error', emit: 'stdout' },
      { level: 'warn', emit: 'stdout' }
    ]
  });

  // Log queries di development
  if (process.env.NODE_ENV === 'development') {
    prisma.$on('query', (e) => {
      fastify.log.debug({
        query: e.query,
        duration: `${e.duration}ms`
      }, 'DB Query');
    });
  }

  await prisma.$connect();
  fastify.log.info('Database connected');

  fastify.decorate('db', prisma);

  fastify.addHook('onClose', async () => {
    await prisma.$disconnect();
  });
}

export default fp(prismaPlugin, { name: 'prisma' });

// plugins/db-postgres.ts β€” Raw PostgreSQL dengan fastify-postgres
import fp from 'fastify-plugin';
import fastifyPostgres from '@fastify/postgres';

export default fp(async (fastify) => {
  await fastify.register(fastifyPostgres, {
    connectionString: process.env.DATABASE_URL
  });

  // Contoh query raw
  fastify.get('/products-raw', async () => {
    const { rows } = await fastify.pg.query(
      'SELECT id, name, price FROM products ORDER BY created_at DESC LIMIT 20'
    );
    return { products: rows };
  });
});

10. Autentikasi dan Otorisasi

Fastify menyediakan plugin @fastify/jwt dan @fastify/auth untuk autentikasi dan otorisasi.

typescript
// plugins/auth.ts
import fp from 'fastify-plugin';
import fastifyJwt from '@fastify/jwt';
import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';

export default fp(async (fastify: FastifyInstance) => {
  // Register JWT plugin
  await fastify.register(fastifyJwt, {
    secret: process.env.JWT_SECRET || 'super-secret-key'
  });

  // Decorator: authenticate
  fastify.decorate('authenticate', async (request: FastifyRequest, reply: FastifyReply) => {
    try {
      await request.jwtVerify();
    } catch (err) {
      reply.code(401).send({
        error: 'Unauthorized',
        message: 'Token tidak valid atau sudah kedaluwarsa'
      });
    }
  });

  // Decorator: requireRole
  fastify.decorate('requireRole', (role: string) => {
    return async (request: FastifyRequest, reply: FastifyReply) => {
      const user = request.user as any;
      if (user.role !== role) {
        reply.code(403).send({
          error: 'Forbidden',
          message: 'Anda tidak memiliki akses ke resource ini'
        });
      }
    };
  });
});

// === routes/auth.ts β€” Login/Register ===
export default async function authRoutes(fastify: FastifyInstance) {
  // POST /auth/register
  fastify.post('/register', {
    schema: {
      body: {
        type: 'object',
        required: ['email', 'password', 'name'],
        properties: {
          email: { type: 'string', format: 'email' },
          password: { type: 'string', minLength: 8 },
          name: { type: 'string', minLength: 2 }
        }
      }
    }
  }, async (request, reply) => {
    const { email, password, name } = request.body as any;

    // Hash password
    const hashedPassword = await fastify.bcrypt.hash(password, 10);

    // Simpan user
    const user = await fastify.db.user.create({
      data: { email, password: hashedPassword, name }
    });

    // Generate JWT
    const token = fastify.jwt.sign(
      { id: user.id, email: user.email, role: user.role },
      { expiresIn: '7d' }
    );

    reply.code(201);
    return { token, user: { id: user.id, email, name } };
  });

  // POST /auth/login
  fastify.post('/login', async (request, reply) => {
    const { email, password } = request.body as any;

    const user = await fastify.db.user.findUnique({ where: { email } });
    if (!user) {
      reply.code(401);
      return { error: 'Email atau password salah' };
    }

    const isValid = await fastify.bcrypt.compare(password, user.password);
    if (!isValid) {
      reply.code(401);
      return { error: 'Email atau password salah' };
    }

    const token = fastify.jwt.sign(
      { id: user.id, email: user.email, role: user.role },
      { expiresIn: '7d' }
    );

    return { token, user: { id: user.id, email, name: user.name } };
  });
}

// === Protected routes ===
fastify.register(async function protectedRoutes(app) {
  // Semua route di plugin ini memerlukan autentikasi
  app.addHook('preHandler', app.authenticate);

  app.get('/profile', async (request) => {
    const user = request.user;
    return { user };
  });

  app.get('/admin/users', {
    preHandler: [app.requireRole('admin')]
  }, async () => {
    return { users: await fastify.db.user.findMany() };
  });
}, { prefix: '/api' });

11. Fastify vs Express.js

Aspek Fastify Express.js Koa.js
Requests/detik~30,000~7,000~8,000
ValidasiJSON Schema built-inManual / middlewareManual / middleware
Serialisasifast-json-stringifyJSON.stringify()JSON.stringify()
ArchitecturePlugin-basedMiddleware-basedMiddleware-based (ctx)
LoggingPino (built-in)Morgan/WinstonManual
TypeScriptβœ… First-class⚠️ @types/⚠️ @types/
Ekosistem🟒 Besar🟒 Sangat besar🟑 Sedang
Maturity🟑 Matang🟒 Sangat matang🟑 Matang

12. Quiz: Uji Pemahamanmu!

Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut untuk menguji pemahamanmu tentang Fastify:

Pertanyaan 1: Mengapa Fastify lebih cepat dari Express.js?

a) Karena menggunakan bahasa pemrograman yang berbeda
b) Karena menggunakan fast-json-stringify untuk serialisasi dan optimasi routing
c) Karena tidak mendukung middleware
d) Karena menjalankan kode di background thread

Pertanyaan 2: Apa keunggulan dari sistem plugin Fastify dibanding middleware Express?

a) Plugin lebih cepat dijalankan
b) Plugin memiliki encapsulation scope sehingga tidak mempengaruhi scope luar
c) Plugin tidak perlu didaftarkan
d) Plugin hanya untuk database

Pertanyaan 3: Apa fungsi dari JSON Schema di Fastify?

a) Hanya untuk dokumentasi API
b) Untuk validasi input DAN serialisasi output yang lebih cepat
c) Untuk mengenerate CSS
d) Untuk mengatur routing

Pertanyaan 4: Kapan hook preHandler dijalankan dalam request lifecycle?

a) Sebelum request masuk ke server
b) Setelah parsing body tapi sebelum validasi
c) Setelah validasi, sebelum handler utama dijalankan
d) Setelah response dikirim ke client

Pertanyaan 5: Logger apa yang digunakan Fastify secara default?

a) Winston
b) Morgan
c) Pino
d) Bunyan
πŸ” Zoom
100%
🎨 Tema