ITSkillsCenter
Blog

Node.js Fastify : tutoriel pratique 2026

13 min de lecture

Lecture : 13 minutes · Niveau : intermédiaire · Mise à jour : avril 2026

Fastify est devenu le framework Node.js de référence pour bâtir des API performantes en 2026. Plus rapide qu’Express, mieux conçu pour async/await, validation native, écosystème de plugins solide. Ce tutoriel montre comment construire une API propre du premier endpoint au déploiement, avec les patterns vraiment utiles en production.

Plutôt qu’une démonstration creuse, l’objectif est de présenter une structure de projet directement réutilisable pour démarrer un nouveau backend PME. Chaque exemple est tiré d’un projet réel, simplifié pour la lisibilité mais conservant les choix structurants. À la fin de ce tutoriel, vous devriez avoir une vision claire de comment organiser votre code, où mettre la validation, comment gérer les erreurs et comment tester votre API sans démarrer de vrai serveur.

Voir aussi → Node.js backend pour PME : guide pratique.


Sommaire

  1. Setup initial avec TypeScript
  2. Premier endpoint et structure
  3. Validation avec Zod
  4. Plugins : étendre Fastify proprement
  5. Hooks pour la cross-cutting logic
  6. Authentification JWT
  7. Gestion d’erreurs centralisée
  8. Tests automatisés
  9. Performance et observabilité
  10. FAQ

1. Setup initial avec TypeScript

mkdir mon-api && cd mon-api
npm init -y
npm install fastify
npm install -D typescript @types/node tsx vitest

tsconfig.json strict (voir tsconfig strict) :

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "resolveJsonModule": true
  }
}

package.json scripts :

{
  "type": "module",
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js",
    "test": "vitest"
  }
}

2. Premier endpoint et structure

src/index.ts :

import { buildServer } from "./server.js";

const app = buildServer();

app.listen({ port: 3000, host: "0.0.0.0" }, (err, address) => {
  if (err) {
    app.log.error(err);
    process.exit(1);
  }
  app.log.info(`Server listening on ${address}`);
});

src/server.ts :

import Fastify from "fastify";
import { clientsRoutes } from "./routes/clients.js";

export function buildServer() {
  const app = Fastify({
    logger: {
      level: "info",
      transport: process.env.NODE_ENV === "development"
        ? { target: "pino-pretty" }
        : undefined,
    },
  });

  app.get("/health", async () => ({ status: "ok" }));

  app.register(clientsRoutes, { prefix: "/api/clients" });

  return app;
}

src/routes/clients.ts :

import { FastifyInstance } from "fastify";

export async function clientsRoutes(app: FastifyInstance) {
  app.get("/", async () => {
    return [{ id: "1", nom: "Acme" }];
  });

  app.get<{ Params: { id: string } }>("/:id", async (request) => {
    return { id: request.params.id, nom: "Acme" };
  });
}

Lancer : npm run dev. Tester : curl http://localhost:3000/api/clients.

Structure recommandée pour grandir

src/
├── index.ts                # entrypoint
├── server.ts               # buildServer
├── env.ts                  # validation Zod env vars
├── routes/
│   ├── clients.ts
│   └── auth.ts
├── services/
├── lib/
├── plugins/
└── schemas/

buildServer séparé de listen permet de tester l’app sans démarrer un serveur réel. Cette séparation est l’une des décisions les plus rentables pour la testabilité d’une API Fastify : tous les tests d’intégration peuvent utiliser app.inject() qui simule une requête HTTP en interne, sans port ouvert ni latence réseau. Les tests deviennent rapides, déterministes, et exécutables en parallèle sans conflit de ports.


3. Validation avec Zod

Fastify supporte JSON Schema nativement, mais Zod est plus expressif et partagé entre frontend et backend.

npm install zod fastify-type-provider-zod
import { ZodTypeProvider, validatorCompiler, serializerCompiler } from "fastify-type-provider-zod";
import { z } from "zod";

const app = Fastify().withTypeProvider<ZodTypeProvider>();
app.setValidatorCompiler(validatorCompiler);
app.setSerializerCompiler(serializerCompiler);

const createClientSchema = z.object({
  nom: z.string().min(2).max(100),
  email: z.string().email(),
  telephone: z.string().regex(/^\+?[0-9 -]{6,20}$/).optional(),
});

const clientResponseSchema = z.object({
  id: z.string(),
  nom: z.string(),
  email: z.string(),
  createdAt: z.string(),
});

app.post("/clients", {
  schema: {
    body: createClientSchema,
    response: { 201: clientResponseSchema },
  },
  handler: async (request, reply) => {
    // request.body est typé Client correctement
    const client = await db.client.create({ data: request.body });
    return reply.code(201).send(client);
  },
});

Avantages :
Validation auto : si le body ne matche pas, Fastify renvoie 400 avec un message clair
Types TypeScript : request.body est correctement typé
Serialization rapide : Fastify utilise les schémas pour produire la réponse efficacement


4. Plugins : étendre Fastify proprement

Le système de plugins de Fastify encapsule la logique. Chaque plugin a son propre scope (DI léger, isolation).

Plugin custom

// src/plugins/db.ts
import fp from "fastify-plugin";
import { PrismaClient } from "@prisma/client";

export default fp(async (app) => {
  const prisma = new PrismaClient();
  await prisma.$connect();
  app.decorate("prisma", prisma);
  app.addHook("onClose", async () => {
    await prisma.$disconnect();
  });
});

declare module "fastify" {
  interface FastifyInstance {
    prisma: PrismaClient;
  }
}

fp (fastify-plugin) marque le plugin comme « non-encapsulé » : ses décorateurs sont visibles depuis le serveur entier.

// Usage
app.get("/clients/:id", async (request) => {
  return await app.prisma.client.findUnique({
    where: { id: request.params.id },
  });
});

Plugins essentiels de l’écosystème

npm install @fastify/cors @fastify/helmet @fastify/rate-limit @fastify/jwt @fastify/cookie
import cors from "@fastify/cors";
import helmet from "@fastify/helmet";
import rateLimit from "@fastify/rate-limit";

await app.register(helmet);
await app.register(cors, {
  origin: ["https://app.exemple.com"],
  credentials: true,
});
await app.register(rateLimit, {
  max: 100,
  timeWindow: "1 minute",
});

5. Hooks pour la cross-cutting logic

Les hooks permettent d’intercepter le cycle de vie des requêtes :

// Avant chaque requête
app.addHook("preHandler", async (request, reply) => {
  request.log.info({ url: request.url }, "incoming");
});

// Après chaque réponse
app.addHook("onResponse", async (request, reply) => {
  request.log.info(
    { statusCode: reply.statusCode, duration: reply.elapsedTime },
    "request done"
  );
});

// Hook ciblé sur certaines routes via plugin scope
async function authPlugin(app) {
  app.addHook("preHandler", verifierToken);
  app.get("/profile", ...);
  app.get("/orders", ...);
}
app.register(authPlugin, { prefix: "/api" });

Liste des hooks principaux

  • onRequest : tout début de requête
  • preParsing : avant parsing du body
  • preValidation : avant validation du schéma
  • preHandler : juste avant le handler
  • preSerialization : avant transformation de la réponse
  • onSend : avant envoi
  • onResponse : après réponse complète
  • onError : sur erreur
  • onClose : à l’arrêt du serveur

6. Authentification JWT

import jwt from "@fastify/jwt";

app.register(jwt, {
  secret: env.JWT_SECRET,
  sign: { expiresIn: "1h" },
});

// Endpoint login
app.post("/login", {
  schema: {
    body: z.object({
      email: z.string().email(),
      password: z.string().min(8),
    }),
  },
  handler: async (request, reply) => {
    const { email, password } = request.body;
    const user = await app.prisma.user.findUnique({ where: { email } });
    if (!user || !await verifyPassword(password, user.passwordHash)) {
      return reply.code(401).send({ error: "INVALID_CREDENTIALS" });
    }
    const token = app.jwt.sign({ sub: user.id, role: user.role });
    return { token };
  },
});

// Middleware d'auth
async function authenticate(request, reply) {
  try {
    await request.jwtVerify();
  } catch {
    reply.code(401).send({ error: "UNAUTHORIZED" });
  }
}

// Usage
app.get("/api/me", { onRequest: [authenticate] }, async (request) => {
  return request.user;
});

Hash de mot de passe

import bcrypt from "bcrypt";

async function hashPassword(plain: string) {
  return bcrypt.hash(plain, 12);
}

async function verifyPassword(plain: string, hash: string) {
  return bcrypt.compare(plain, hash);
}

12 rounds est un bon compromis 2026 (~250ms par hash). Argon2 est encore plus moderne via argon2 npm.


7. Gestion d’erreurs centralisée

// Classes custom
export class AppError extends Error {
  constructor(
    message: string,
    public statusCode: number,
    public code: string,
  ) {
    super(message);
  }
}

export class NotFoundError extends AppError {
  constructor(resource: string) {
    super(`${resource} not found`, 404, "NOT_FOUND");
  }
}

export class ValidationError extends AppError {
  constructor(message: string, public details?: unknown) {
    super(message, 400, "VALIDATION_ERROR");
  }
}

// Handler global
app.setErrorHandler((err, request, reply) => {
  if (err instanceof AppError) {
    return reply.code(err.statusCode).send({
      error: err.code,
      message: err.message,
    });
  }

  // Erreur Zod (validation native)
  if (err.validation) {
    return reply.code(400).send({
      error: "VALIDATION_ERROR",
      details: err.validation,
    });
  }

  // Erreur inattendue : ne pas leaker
  request.log.error({ err }, "unhandled error");
  return reply.code(500).send({ error: "INTERNAL_ERROR" });
});

// Usage dans une route
app.get("/clients/:id", async (request) => {
  const client = await app.prisma.client.findUnique({
    where: { id: request.params.id },
  });
  if (!client) throw new NotFoundError("Client");
  return client;
});

Cette approche centralise la transformation erreur → réponse HTTP. Les routes ne se soucient plus de la forme HTTP des erreurs.


8. Tests automatisés

// tests/clients.test.ts
import { describe, it, expect, beforeEach, afterAll } from "vitest";
import { buildServer } from "../src/server.js";

describe("Clients API", () => {
  const app = buildServer();

  afterAll(async () => {
    await app.close();
  });

  it("GET /health returns ok", async () => {
    const res = await app.inject({ method: "GET", url: "/health" });
    expect(res.statusCode).toBe(200);
    expect(res.json()).toEqual({ status: "ok" });
  });

  it("POST /api/clients creates a client", async () => {
    const res = await app.inject({
      method: "POST",
      url: "/api/clients",
      payload: { nom: "Acme", email: "contact@acme.test" },
    });
    expect(res.statusCode).toBe(201);
    const body = res.json();
    expect(body.id).toBeDefined();
    expect(body.nom).toBe("Acme");
  });

  it("POST /api/clients rejects invalid email", async () => {
    const res = await app.inject({
      method: "POST",
      url: "/api/clients",
      payload: { nom: "Acme", email: "not-an-email" },
    });
    expect(res.statusCode).toBe(400);
  });
});

app.inject simule une requête HTTP sans démarrer de serveur réel — rapide et déterministe.

Base de données pour tests

Pour des tests d’intégration avec une vraie DB : SQLite en mémoire pour la simplicité, ou PostgreSQL dans un container Docker dédié aux tests. testcontainers automatise le second.


9. Performance et observabilité

Logger Pino

Fastify utilise Pino par défaut. Logs JSON structurés, performants.

const app = Fastify({
  logger: {
    level: env.LOG_LEVEL,
    redact: {
      paths: ["headers.authorization", "body.password"],
      remove: true,
    },
  },
});

Métriques Prometheus

import metrics from "fastify-metrics";

await app.register(metrics, { endpoint: "/metrics" });

Endpoint /metrics expose les métriques au format Prometheus, prêtes pour scraping.

Compression

import compress from "@fastify/compress";

app.register(compress, { encodings: ["gzip", "br"] });

Réduit drastiquement la taille des réponses JSON volumineuses.

Schema-based serialization

Spécifier la response schema (vu plus haut) accélère la sérialisation : Fastify utilise un compilateur dédié plus rapide que JSON.stringify générique.

Voir aussi → Node.js déploiement production pour les bonnes pratiques opérationnelles.

Graceful shutdown

Pour des déploiements modernes (Kubernetes, Docker), le serveur doit s’arrêter proprement quand un signal SIGTERM arrive : refuser les nouvelles requêtes mais terminer celles en cours, fermer les connexions DB, libérer les ressources.

const signals = ["SIGINT", "SIGTERM"] as const;
for (const signal of signals) {
  process.on(signal, async () => {
    app.log.info(`Received ${signal}, closing gracefully`);
    await app.close();
    process.exit(0);
  });
}

Sans graceful shutdown : requêtes coupées en plein traitement, connexions DB pendantes, données potentiellement corrompues. Indispensable pour de la production sérieuse.


10. FAQ

Fastify est-il production-ready ?

Oui depuis longtemps. Utilisé par Microsoft, Capital One, Mastercard, et de nombreuses startups. Stable, performant, bien maintenu.

Comment migrer un projet Express vers Fastify ?

Pas de migration automatique. Approche progressive : utiliser un reverse proxy (Nginx) qui route certaines URLs vers le nouveau Fastify et le reste vers l’ancien Express. Migrer endpoint par endpoint. La plupart des middlewares Express ont un équivalent Fastify (@fastify/cors, @fastify/helmet, etc.).

Plugin encapsulé vs fastify-plugin ?

Un plugin classique a son propre scope : ses décorateurs ne sont pas visibles à l’extérieur. fp(...) casse cette encapsulation et expose globalement. Utiliser fp pour des plugins « globaux » (DB, logger, env), classique pour des routes ou middlewares ciblés.

Comment gérer l’upload de fichiers ?

@fastify/multipart parse les uploads multipart. Pour de gros fichiers : streamer plutôt que charger en mémoire. Pour stockage : S3-compatible (Hetzner Storage Box, Backblaze B2, AWS S3) avec le SDK approprié.

Fastify et WebSockets ?

@fastify/websocket gère les WebSockets. Pour des cas plus complexes (rooms, salons), socket.io existe en plugin Fastify aussi.

Comment debugger une route lente ?

Les logs Fastify incluent responseTime. Identifier les routes lentes, puis profiler avec clinic.js ou node --prof pour analyser le hotspot. Souvent : requêtes DB N+1, parsing JSON volumineux, calculs synchrones bloquants.

Single instance ou cluster ?

Single instance suffit pour des charges modérées (< 1000 req/s typiquement). Au-delà : plusieurs instances horizontales derrière un load balancer (Nginx, Caddy) — plus résilient et simple qu’un cluster Node.js intégré.


Articles liés (cluster Node.js backend)


Article mis à jour le 25 avril 2026. Pour signaler une erreur ou suggérer une amélioration, écrivez-nous.

Besoin d'un site web ?

Confiez-nous la Création de Votre Site Web

Site vitrine, e-commerce ou application web — nous transformons votre vision en réalité digitale. Accompagnement personnalisé de A à Z.

À partir de 250.000 FCFA
Parlons de Votre Projet
Publicité