Skip to content
Learni
View all tutorials
Backend

How to Master Fastify for Ultra-Performant APIs in 2026

Lire en français

Introduction

Fastify is the most performant Node.js framework in 2026, outperforming Express by 2 to 3 times in throughput thanks to its zero-dependency architecture and asynchronous plugin system. Ideal for scalable APIs under high load, it excels in native JSON schema validation (via Ajv), hooks for fine-grained request interception, and the official plugin ecosystem like @fastify/jwt or @fastify/rate-limit.

This advanced tutorial guides you step by step to build a complete API: JWT authentication, rate limiting, structured logging, custom error handling, and clustering for production. Each step includes functional TypeScript code ready to copy-paste. By the end, you'll master expert patterns for APIs that handle millions of requests per day without breaking a sweat. Why Fastify? It reduces JSON parsing by 80% and supports TypeScript natively, making deployments more reliable and faster. (148 words)

Prerequisites

  • Node.js 20+ (with npm or yarn)
  • Advanced knowledge of TypeScript and Node.js
  • An editor like VS Code with the TypeScript extension
  • Testing tools: Tap (included with Fastify) or Jest
  • Optional database (e.g., PostgreSQL for demo)

Installation and Initial Setup

terminal
mkdir fastify-advanced-api && cd fastify-advanced-api
npm init -y
npm install fastify@latest @fastify/rate-limit @fastify/jwt @fastify/swagger @fastify/cors @fastify/autoload
npm install -D @types/node typescript tsx
npm install @fastify/merge-json-schemas

echo '{
  "type": "module",
  "scripts": {
    "dev": "tsx watch src/server.ts",
    "start": "tsx src/server.ts"
  }
}' > package.json

mkdir src
mkdir src/plugins
mkdir src/routes
mkdir src/schemas

This command sets up a modern ESM project with essential plugins: rate-limit for DDoS protection, JWT for auth, Swagger for auto-docs, CORS for frontends. tsx enables direct TypeScript execution without builds. The folder structure follows Fastify best practices (autoload for routes/plugins).

Fastify Project Structure

Fastify encourages a modular architecture: src/server.ts as the entry point, plugins/ for global services (e.g., logger, auth), routes/ for handlers, schemas/ for JSON validation. Use register to compose the app like Lego blocks, avoiding callback hell.

Basic Server with Autoload and CORS

src/server.ts
import Fastify from 'fastify';
import cors from '@fastify/cors';
import autoload from '@fastify/autoload';
import path from 'path';

const fastify = Fastify({
  logger: {
    level: 'info',
    transport: {
      target: 'pino-pretty',
      options: { colorize: true }
    }
  }
});

await fastify.register(cors, { origin: '*' });
await fastify.register(autoload, {
  dir: path.join(__dirname, 'plugins'),
  options: Object.assign({ prefix: '/api' })
});
await fastify.register(autoload, {
  dir: path.join(__dirname, 'routes'),
  options: Object.assign({ prefix: '/api' })
});

fastify.get('/health', async (request, reply) => {
  return { status: 'OK', timestamp: new Date().toISOString() };
});

const start = async () => {
  try {
    await fastify.listen({ port: 3000, host: '0.0.0.0' });
    fastify.log.info(`Serveur démarré sur http://localhost:3000`);
  } catch (err) {
    fastify.log.error(err);
    process.exit(1);
  }
};

start();

This server enables the fast Pino logger, CORS for cross-origin calls, and autoload to automatically load all plugins/routes from /plugins and /routes. The listen hook handles startup errors. Test with npm run dev: curl http://localhost:3000/health returns valid JSON.

Global Rate Limiting Plugin

src/plugins/rateLimit.ts
import rateLimit from '@fastify/rate-limit';

export default async function (fastify: any) {
  await fastify.register(rateLimit, {
    max: 100,
    timeWindow: '1 minute',
    keyGenerator: (request: any) => {
      return request.ip || request.headers['x-forwarded-for'] || 'unknown';
    },
    skipSuccessfulRequests: false,
    onExceeding: async (request: any, reply: any) => {
      reply.code(429).send({
        error: 'Trop de requêtes',
        retryAfter: 60
      });
    }
  });

  fastify.addHook('preHandler', async (request: any, reply: any) => {
    await fastify.rateLimit(request, reply);
  });
}

This plugin enforces a rate limit of 100 requests per minute per IP, using a preHandler hook to run before every route. keyGenerator gets the real IP behind proxies. On exceeding, it returns a custom 429. Perfect for abuse protection without performance hits.

Advanced JSON Schema Validation

Analogy: Fastify schemas act like JSON Schema contracts: they automatically validate inputs/outputs, rejecting bad payloads before the handler (saving CPU). Use $merge to compose reusable schemas.

Reusable Schemas and Validated Routes

src/routes/users.ts
import { FastifyInstance } from 'fastify';
import { createUserSchema, getUserSchema } from '../schemas/user.js';

export async function usersRoutes(fastify: FastifyInstance) {
  fastify.post('/users', { schema: createUserSchema }, async (request, reply) => {
    const { email, password, name } = request.body as any;
    // Simule DB insert
    const userId = 'uuid-' + Date.now();
    reply.code(201).send({ id: userId, email });
  });

  fastify.get('/users/:id', { schema: getUserSchema }, async (request) => {
    const { id } = request.params as any;
    return { id, email: 'user@example.com', name: 'John Doe' };
  });
}

Schemas automatically validate body/params/response (400/500 on invalid). format: 'email' and minLength leverage powerful Ajv validators. The /api/users route is protected: test with curl -X POST ... and a bad email → structured 400 error.

JWT Plugin with Auth Hooks

src/plugins/auth.ts
import jwt from '@fastify/jwt';

export default async function (fastify: any) {
  await fastify.register(jwt, {
    secret: 'supersecret2026',
    sign: { expiresIn: '1h' }
  });

  fastify.decorate('authenticate', async function (request: any, reply: any) {
    try {
      await request.jwtVerify();
    } catch (err) {
      reply.send(err);
    }
  });

  fastify.addHook('preValidation', async (request: any, reply: any) => {
    if (request.url.startsWith('/api/protected')) {
      await request.jwtVerify();
    }
  });
}

This plugin adds global jwtVerify() and a reusable authenticate decorator. The preValidation hook auto-protects /protected routes. Generate a token via a login route (to add), then use Authorization: Bearer . Eliminates boilerplate in handlers.

Advanced Hooks and Logging

Fastify hooks (onRequest, preHandler, preValidation, postHandler) intercept the request lifecycle like middleware but asynchronously and in parallel. Pair them with Pino logger for structured traces: { reqId, level, msg }.

Custom Error Handling and Swagger

src/plugins/swagger.ts
import swagger from '@fastify/swagger';
import swaggerUI from '@fastify/swagger-ui';

export default async function (fastify: any) {
  await fastify.register(swagger, {
    openapi: {
      info: {
        title: 'Fastify Advanced API',
        version: '1.0.0'
      }
    }
  });
  await fastify.register(swaggerUI, {
    routePrefix: '/docs'
  });
};

fastify.setErrorHandler((error: any, request: any, reply: any) => {
  const statusCode = error.statusCode || 500;
  fastify.log.error({ err: error, reqId: request.id }, 'Erreur');
  reply.code(statusCode).send({
    error: error.message,
    timestamp: new Date().toISOString()
  });
});

Swagger generates interactive docs at /docs. The global ErrorHandler catches all errors, logs with request.id (auto-generated), and returns clean JSON. Add it to server.ts after registers. Test: trigger an error → structured log + 500 response.

Clustering for Production

src/server.prod.ts
import Fastify from 'fastify';
import cluster from 'cluster';
import os from 'os';

const numCPUs = os.cpus().length;

if (cluster.isPrimary) {
  console.log(`Master démarré, fork ${numCPUs} workers`);
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }
  cluster.on('exit', (worker) => {
    console.log(`Worker ${worker.process.pid} mort`);
    cluster.fork();
  });
} else {
  // Collez ici le code de server.ts (imports, fastify instance, listen)
  const fastify = Fastify({ logger: true });
  // ... tous les registers ...
  await fastify.listen({ port: 3000, host: '0.0.0.0' });
}

Uses Node's native cluster module to scale across all CPU cores. The primary forks workers; exit handler restarts crashed ones (zero-downtime). In production, boosts throughput 4-8x. Run with tsx src/server.prod.ts.

Best Practices

  • Plugins before routes: Always register(plugins) then autoload(routes) for execution order.
  • Schemas per route: Validate responses for strict API contracts (OpenAPI compliant).
  • Lightweight hooks: Avoid DB calls in onRequest; reserve preHandler for auth.
  • Structured logging: Use request.log everywhere to correlate req/err.
  • Unit tests: Inject Fastify into fastify.inject() for serverless tests.

Common Pitfalls to Avoid

  • Forgetting await on register(): Causes silently unloaded routes.
  • Ignoring schema conflicts: Use @fastify/merge-json-schemas for $merge.
  • Rate limit without custom keyGenerator: Fails behind Cloudflare/NGINX.
  • No clustering in prod: Wastes 80% of multi-core CPUs.

Next Steps

  • Advanced plugins: @fastify/postgres for pooled DB, @fastify/redis for caching.
  • Benchmark: autocannon -c 100 -d 20 http://localhost:3000/api/health.
  • Deployment: Docker + PM2 or fastify-cluster.
Discover our advanced Node.js trainings to master Fastify in depth.