Node.js Framework

Fastify

Fastify est un framework web Node.js rapide et basé sur le concept de "schema-first". JSON Schema compile votre validation et votre sérialisation, tandis que les plugins encapsulés garantissent la cohérence des services de grande taille.

intermediate15 min readUpdated 16 sept. 2026
server.ts
ts
// server.ts
import Fastify from "fastify";
import { Type } from "@sinclair/typebox";

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

const User = Type.Object({
  id: Type.String({ format: "uuid" }),
  name: Type.String({ minLength: 1 }),
  email: Type.String({ format: "email" }),
});

app.get("/users/:id", {
  schema: {
    params: Type.Object({ id: Type.String({ format: "uuid" }) }),
    response: { 200: User },
  },
}, async (request, reply) => {
  const user = await db.users.findById(request.params.id);
  if (!user) return reply.callNotFound();
  return user;
});

await app.listen({ port: 3000, host: "0.0.0.0" });
Sortie
2016
S'exécute sur
Node.js
Style
Schema-first, basé sur les plugins
Idée centrale
JSON Schema
Langage
JavaScript / TypeScript
Version
5.x

Pourquoi c'est important

Pourquoi Fastify domine en termes de débit

Débit optimisé par design

Grâce à un routeur radix-tree et des schémas compilés, Fastify traite bien plus de requêtes par seconde qu'Express tout en conservant une API familière.

Validation compilée

Un JSON Schema n'est pas qu'une simple documentation. Fastify le compile en un validateur et un sérialiseur, ainsi les entrées invalides sont rejetées avant même l'exécution de votre handler.

Plugins encapsulés

Chaque plugin possède son propre scope. Les décorateurs et les hooks restent locaux sauf si vous les partagez explicitement, ce qui évite les fuites d'état dans les services complexes.

Le tableau complet

Les trois piliers de Fastify

Déclarez la structure de vos données, enveloppez vos fonctionnalités dans des plugins encapsulés et interceptez la requête à des points précis du cycle de vie.

JSON Schema

Déclarer

Décrivez une seule fois les paramètres, le corps, la requête et les réponses, et Fastify s'occupe de les valider et de les sérialiser pour vous.

Plugins

Encapsuler

Un plugin est une fonction capable de décorer l'instance et d'enregistrer des routes à l'intérieur de son propre scope.

Hooks

Intercepter

Les hooks de cycle de vie comme onRequest, preHandler et onSend s'exécutent à des moments précis de chaque requête.

Le guide complet

Fastify: Tout ce que vous devez savoir

Qu’est-ce que Fastify ?

Fastify est un framework web pour Node.js basé sur un pari simple : si vous décrivez vos données, le framework peut s’occuper d’une plus grande partie du travail. Il associe un routeur très rapide à un système de schémas, un modèle de plugins à portée définie (scoped) et un cycle de vie composé de hooks. Le résultat est un framework à la fois performant et rigoureux sur la sécurité, sans pour autant vous imposer une structure de dossiers spécifique.

Apparu en 2016, il a atteint sa version stable 1.0 en 2018. Aujourd’hui, il propulse des API en production pour des entreprises qui recherchent l’ergonomie d’Express, mais avec des performances nettement supérieures et une véritable stratégie de validation. Si vous connaissez Express, la majeure partie de Fastify vous semblera familière en l’espace d’un après-midi.

Schema-first : une validation compilée

La caractéristique principale est que les schémas sont exécutables. Lorsque vous associez un JSON Schema à une route, Fastify le compile une seule fois au démarrage et utilise le validateur compilé pour chaque requête. Aucune interprétation n’est effectuée à chaque appel, c’est pourquoi la validation est quasiment gratuite.

import { Type, type Static } from "@sinclair/typebox";

const CreateUser = Type.Object({
  name: Type.String({ minLength: 1, maxLength: 80 }),
  email: Type.String({ format: "email" }),
});

type CreateUser = Static<typeof CreateUser>;

app.post("/users", {
  schema: { body: CreateUser },
}, async (request) => {
  // request.body is validated and typed as CreateUser
  return createUser(request.body);
});

Cela offre deux avantages. Premièrement, les requêtes invalides sont rejetées avec un 400 avant même que votre handler ne soit appelé, ainsi votre logique métier ne traite que des données propres. Deuxièmement, ce même schéma pilote la sérialisation : Fastify compile le schéma de réponse en une fonction fast-json-stringify, ce qui est considérablement plus rapide que JSON.stringify et garantit que vous ne divulguez jamais de champs que vous n’avez pas déclarés.

Routes et options de route

Une route se compose d’une méthode, d’un chemin et d’un handler, mais la partie intéressante réside dans l’objet d’options situé entre les deux. C’est là que se trouvent le schéma, le handler et les métadonnées propres à chaque route.

app.route({
  method: "GET",
  url: "/health",
  config: { public: true },
  schema: {
    response: {
      200: Type.Object({ status: Type.String() }),
    },
  },
  handler: async () => ({ status: "ok" }),
});

Les méthodes raccourcies (app.get, app.post et ainsi de suite) acceptent les mêmes options en second argument. Les paramètres de route arrivent sur request.params, la query string sur request.query, et le corps analysé (parsed body) sur request.body. Comme les structures proviennent du schéma, TypeScript reconnaît ces trois éléments.

reply constitue l’autre moitié du handler. reply.code(404), reply.header(...) et reply.send(...) s’inspirent d’Express, et retourner une valeur depuis un handler async est un raccourci pour reply.send. Fastify propose également des helpers tels que reply.callNotFound() afin que la gestion des erreurs reste cohérente.

Plugins et encapsulation

Fastify ne possède pas de chaîne de middleware au sens d’Express. À la place, tout est un plugin, et chaque plugin dispose de son propre scope. Cette règle unique est ce qui permet aux applications de grande taille de rester prévisibles.

import fp from "fastify-plugin";

const dbPlugin = fp(async (app) => {
  app.decorate("users", createUserRepository(app.log));
}, { name: "db" });

await app.register(dbPlugin);

Sans fastify-plugin, les décorateurs et les hooks ajoutés à l’intérieur du plugin ne seraient visibles que pour les routes enregistrées dans ce même plugin. C’est ce qu’on appelle l’encapsulation : une fonctionnalité peut posséder ses propres dépendances et sa configuration sans polluer le reste de l’application. L’utilisation de fp permet de supprimer délibérément cette frontière lorsque vous construisez une infrastructure partagée, comme une base de données ou un logger.

Les décorateurs sont la manière idiomatique d’attacher des fonctionnalités : app.decorate("users", repo) pour l’instance, app.decorateRequest("user", null) pour la requête, et app.decorateReply pour la réponse. Comme ils sont typés via l’augmentation de module, vous bénéficiez de l’autocomplétion au lieu de any.

Le cycle de vie : l’ordre des hooks

Les hooks vous permettent d’exécuter du code à des moments précis sans avoir à encapsuler vos handlers. Ils s’exécutent selon un ordre fixe :

  1. onRequest — le point le plus précoce, idéal pour l’authentification et les IDs de requête.
  2. preParsing — avant la lecture du corps de la requête, pour la compression ou la vérification de la taille.
  3. preValidation — après le parsing, avant la validation du schéma.
  4. preHandler — après la validation, juste avant le handler.
  5. preSerialization et onSend — façonnent le payload lors de la réponse.
  6. onResponse et onError — observent la requête une fois terminée.
app.addHook("onRequest", async (request) => {
  request.start = process.hrtime.bigint();
});

app.addHook("onResponse", async (request, reply) => {
  const ms = Number(process.hrtime.bigint() - request.start) / 1e6;
  request.log.info({ ms }, "request completed");
});

Les hooks sont scopés comme les plugins ; ainsi, un hook ajouté à l’intérieur d’un plugin ne s’exécutera que pour les routes de ce plugin. onClose est l’équivalent pour l’arrêt du serveur, et c’est là que vous libérez les pools de base de données et les timers. Maîtriser cet ordre est essentiel ; la documentation le liste précisément et il change rarement.

TypeScript sans le superflu

Fastify est écrit en TypeScript et ses types sont traités comme des citoyens de première classe. Vous typez une route en passant des paramètres génériques, et les erreurs de validation apparaissent à la compilation plutôt qu’en production.

app.get<{
  Params: { id: string };
  Querystring: { fields?: string };
}>("/users/:id", async (request) => {
  const { id } = request.params;
  const { fields } = request.query;
  return findUser(id, fields);
});

Le pattern le plus précieux est celui des type providers. Avec @fastify/type-provider-typebox, le schéma lui-même devient le type ; ainsi, vous n’avez jamais à écrire l’interface deux fois et vous évitez tout risque de désynchronisation entre les deux.

import { TypeBoxTypeProvider } from "@fastify/type-provider-typebox";

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

app.post("/users", { schema: { body: CreateUser } }, async (request) => {
  return createUser(request.body); // typed as CreateUser
});

L’augmentation de module est la méthode utilisée pour typer les décorateurs : déclarez la propriété sur FastifyInstance ou FastifyRequest, et elle sera disponible partout. Lorsqu’un élément est unknown, c’est généralement le signe qu’un schéma ou une déclaration est manquant.

Logging avec pino, intégré nativement

Fastify est livré avec pino comme logger. Activez-le via une seule option et chaque requête générera une ligne de log JSON structurée comprenant un id, le timing et vos propres champs.

const app = Fastify({
  logger: {
    level: "info",
    redact: ["req.headers.authorization"],
  },
});

app.get("/orders", async (request) => {
  request.log.info({ userId: request.user?.id }, "listing orders");
  return listOrders(request.user?.id);
});

Comme il s’agit de pino, la sortie est du JSON délimité par des sauts de ligne (newline-delimited JSON) qui s’intègre parfaitement à n’importe quel agrégateur de logs, et request.log inclut automatiquement le contexte de la requête. En développement, passez la sortie par pino-pretty pour un affichage lisible ; en production, conservez le format JSON. Les règles de redaction sont le moyen le plus sûr d’empêcher les tokens de se retrouver dans vos logs.

Tester avec app.inject()

Il n’est pas nécessaire d’ouvrir un port pour tester une application Fastify. app.inject() exécute une requête à travers toute la pile, incluant le routage, la validation et les hooks, et renvoie un objet de réponse sur lequel vous pouvez effectuer des assertions.

const app = buildApp();
await app.ready();

const res = await app.inject({
  method: "POST",
  url: "/users",
  payload: { name: "Ada", email: "[email protected]" },
});

assert.equal(res.statusCode, 201);
assert.equal(res.json().name, "Ada");
await app.close();

Deux bonnes pratiques rendent ce processus agréable. Premièrement, exportez une factory buildApp() depuis app.ts et n’appelez listen() que dans server.ts, afin que les tests ne bind jamais de port. Deuxièmement, appelez await app.ready() avant l’injection, ce qui force le chargement complet des plugins et des schémas. Les tests s’exécutent rapidement car il n’y a pas de socket, et ils sont isolés car chaque test construit sa propre instance.

Bonnes pratiques

  • Définissez des schémas de requête et de réponse pour chaque route, puis dérivez les types à l’aide d’un type provider.
  • Séparez la factory de l’application et le listener dans des fichiers distincts pour que les tests restent in-process.
  • Enregistrez l’infrastructure avec fastify-plugin et le code métier sans, afin que les scopes restent cohérents.
  • Utilisez onRequest pour l’authentification et preHandler pour l’autorisation nécessitant le corps de la requête analysé.
  • Centralisez le formatage des erreurs avec setErrorHandler au lieu d’ajouter des conditions dans chaque handler.
  • Loguez des champs structurés, masquez les secrets et ne loguez jamais l’intégralité des corps de requête.
  • Validez la configuration avec @fastify/env et provoquez un arrêt immédiat (fail fast) au démarrage en cas d’erreur.
  • Appelez app.close() dans les tests et lors du SIGTERM pour que les connexions soient fermées proprement.

Erreurs courantes

  • Enregistrer un plugin sans fastify-plugin et se demander pourquoi les décorateurs sont indéfinis ailleurs.
  • Oublier await app.ready() dans les tests, ce qui fait que les schémas et les plugins ne sont pas encore chargés.
  • Utiliser JSON.stringify manuellement alors qu’un schéma de réponse permettrait une sérialisation plus rapide et plus sûre.
  • Omettre les schémas de réponse, ce qui signifie que n’importe quelle propriété peut fuiter vers les clients.
  • Ajouter des hooks globaux alors qu’un hook de plugin scoped éviterait d’impacter des routes non liées de manière imprévue.
  • Traiter les hooks comme du middleware et s’attendre à ce que preHandler s’exécute avant le parsing du corps de la requête.
  • Ignorer la structure du gestionnaire d’erreurs par défaut et casser les clients de l’API avec des réponses incohérentes.

Et après ?

Fastify est l’évolution naturelle après Express lorsque le débit et la validation deviennent prioritaires. Si votre équipe recherche une architecture plus directive avec l’injection de dépendances basée sur Fastify, consultez le guide NestJS. Pour une approche similaire basée sur les standards du web pour les runtimes edge, découvrez Hono. Enfin, si le cycle de vie des plugins vous semble encore abstrait, n’hésitez pas à revoir les bases de Node.js sur lesquelles il s’appuie.

En pratique

Routes, plugins et hooks

Les quatre composants d'un service Fastify : une route typée, une dépendance encapsulée, un hook de cycle de vie et un test via inject.

routes/users.ts
import type { FastifyPluginAsync } from "fastify";
import { Type, type Static } from "@sinclair/typebox";

const User = Type.Object({
  id: Type.String({ format: "uuid" }),
  name: Type.String({ minLength: 1 }),
  email: Type.String({ format: "email" }),
});

const Params = Type.Object({
  id: Type.String({ format: "uuid" }),
});

export const userRoutes: FastifyPluginAsync = async (app) => {
  app.get<{ Params: Static<typeof Params> }>(
    "/users/:id",
    {
      schema: { params: Params, response: { 200: User } },
    },
    async (request, reply) => {
      const user = await app.users.findById(request.params.id);
      if (!user) return reply.callNotFound();
      return user;
    },
  );
};

Déclaration du contrat

Un schéma valide et sérialise en une seule déclaration. Les vérifications manuelles dispersent les mêmes règles dans chaque handler et divergent avec le temps.

Préférer
app.post("/users", {
  schema: {
    body: Type.Object({
      name: Type.String({ minLength: 1 }),
      email: Type.String({ format: "email" }),
    }),
  },
}, createUser);
Éviter
app.post("/users", async (request, reply) => {
  const { name, email } = request.body as any;
  if (typeof name !== "string") {
    return reply.code(400).send({ error: "invalid" });
  }
  // validation grows with every field
});

Partage d'une dépendance

Utilisez un décorateur à l'intérieur d'un plugin pour garder la surface de l'instance explicite. Attacher des propriétés à la main est invisible pour le cycle de vie de Fastify.

Préférer
export default fp(async (app) => {
  app.decorate("users", createUserRepository());
});
Éviter
// Mutating the instance directly skips decorators,
// typing and the onClose lifecycle.
(app as any).users = createUserRepository();

Compromis

Fastify vaut-il la rigueur des schémas ?

Fastify échange un peu plus de déclarations contre de la vitesse, de la sécurité et de la structure. Ce coût n'est perceptible que sur les très petits projets.

Strengths

  • Rapide pour de bonnes raisons

    Le routeur et les sérialiseurs compilés sont réellement plus performants, permettant au même matériel de supporter plus de trafic sans réécriture.

  • Validation et doc via une source unique

    Le schéma utilisé pour la validation peut également générer un document OpenAPI et des types TypeScript, garantissant la synchronisation des contrats.

  • Une structure scalable

    L'encapsulation des plugins donne à chaque fonctionnalité son propre scope. Les dépendances sont enregistrées une fois et réutilisées délibérément.

Trade-offs

  • Apprentissage des schémas

    JSON Schema est plus verbeux qu'un objet Zod, et les messages d'erreur nécessitent une personnalisation pour être conviviaux pour les consommateurs de l'API.

  • Écosystème plus restreint

    Il existe un plugin pour presque tout, mais beaucoup moins de réponses sur Stack Overflow qu'avec Express, ce qui oblige à consulter la documentation plus souvent.

  • Une rigueur surprenante

    Les schémas de réponse suppriment les propriétés inconnues par défaut. C'est une fonctionnalité, mais cela peut ressembler à une perte de données avant de comprendre la sérialisation.

FAQ

Foire aux questions

Keep learning

Related topics from the roadmap.

$ commencer à apprendre

Prêt à apprendre Fastify ?

Notre tutoriel interactif vous guide à travers Fastify pas à pas — avec des quiz et du vrai code que vous pouvez exécuter dans le navigateur.