Comment Construire une API REST avec Fastify
Construisez une API REST Fastify v5 avec Prisma, validation JSON Schema, plugins, décorateurs, hooks et typage TypeBox pour des posts CRUD.
Fastify est un framework web Node.js haute performance et à faible surcharge, dont la caractéristique principale est la validation et la sérialisation pilotées par JSON Schema : vous déclarez la structure de chaque requête et réponse, et Fastify compile ces schémas en fonctions optimisées au démarrage. Selon les mainteneurs, Fastify est l’un des frameworks web les plus rapides du marché, pouvant traiter jusqu’à 76 000 requêtes par seconde ou plus, selon la complexité du code. Ce tutoriel construit une API REST CRUD complète pour une ressource posts avec Fastify v5 et Prisma comme couche de données, en se concentrant sur les quatre primitives qui distinguent Fastify d’Express — les plugins encapsulés, les décorateurs, la validation/sérialisation par schéma, et les hooks de cycle de vie.
Points Clés
- Fastify v5 requiert Node.js v20 ou supérieur ; la v4 a atteint sa fin de vie le 30 juin 2025, et les versions v3 et antérieures ne sont plus maintenues, donc les nouvelles API doivent démarrer sur la branche v5.x.
- Un schéma
responseFastify remplit un double rôle — il accélère la sérialisation en compilant un stringifier dédié, et il supprime toute propriété non déclarée dans le schéma, empêchant ainsi les champs internes de fuiter dans les réponses. - À partir de la v5, le raccourci de schéma a été supprimé : chaque schéma
querystring,params,bodyetresponsedoit être un JSON Schema complet incluant une propriététype. - Partagez un client de base de données entre les routes en encapsulant le plugin dans
fastify-pluginpour rompre délibérément l’encapsulation, puis en attachant le client avecfastify.decorate('prisma', client). - Avec le type provider TypeBox, vous déclarez chaque schéma une seule fois et Fastify en déduit les types de requête et de réponse, éliminant tout décalage entre les règles de validation et les types TypeScript.
Pourquoi Fastify : les quatre primitives qu’Express ne possède pas
Vous pouvez écrire les mêmes endpoints CRUD avec Express. Ce que vous ne pouvez pas reproduire facilement, c’est le modèle de Fastify, construit sur quatre primitives : les plugins encapsulés, les décorateurs, la validation et sérialisation par schéma, et les hooks de cycle de vie. Les mainteneurs soulignent que Fastify est entièrement extensible via ses hooks, plugins et décorateurs. Il ne s’agit pas de fonctionnalités ajoutées en complément d’un routeur ; ce sont les fondements de l’architecture.
| Problématique | Express (approche typique) | Fastify (approche idiomatique) |
|---|---|---|
| Validation des entrées | Manuelle ou via middleware (express-validator) | JSON Schema déclaré sur la route |
| Sérialisation des réponses | JSON.stringify | fast-json-stringify compilé à partir d’un schéma de réponse |
| Partage de ressources | Middleware global sur un req partagé | Plugins encapsulés + décorateurs |
| Cycle de vie des requêtes | Chaîne de middleware linéaire | Hooks typés (onRequest, preHandler, onError, …) |
L’argument de performance est mécanique, pas marketing. Bien que ce ne soit pas obligatoire, Fastify recommande d’utiliser JSON Schema pour valider les routes et sérialiser les sorties ; en interne, Fastify compile le schéma en une fonction hautement performante. Un schéma de réponse est transformé en sérialiseur spécialisé au démarrage, ce qui permet de produire du JSON en contournant le chemin générique de JSON.stringify. Considérez les chiffres officiels pour ce qu’ils sont : ces benchmarks sont réalisés à l’aide d’un test synthétique de type « hello world » visant à évaluer la surcharge du framework — mesurez les performances de votre propre application avant de citer ces chiffres.
Configuration du projet et serveur v5 minimal
Discover how at OpenReplay.com.
Commencez avec ESM, car Fastify et Prisma v7 sont tous deux ESM en priorité. Fastify v5 ne prend en charge que Node.js v20+ ; si vous utilisez une version plus ancienne de Node.js, vous devrez effectuer une mise à niveau pour utiliser Fastify v5.
mkdir fastify-posts-api && cd fastify-posts-api
npm init -y
npm pkg set type="module"
npm i fastify@5
La version stable actuelle est la 5.9.0, publiée le 28 juin 2026. Créez server.js :
import Fastify from 'fastify'
const app = Fastify({ logger: true })
app.get('/health', async () => ({ status: 'ok' }))
try {
await app.listen({ port: 3000 })
} catch (err) {
app.log.error(err)
process.exit(1)
}
Notez la forme objet de l’appel à listen. La signature à arguments variables de la méthode .listen() a été supprimée en v5, vous ne pouvez donc plus appeler .listen() avec un nombre variable d’arguments — passez toujours un objet d’options tel que { port: 3000 }. Lancez-le avec node --watch server.js et accédez à http://localhost:3000/health.
Le routage à la manière de Fastify : plugins et encapsulation
Dans Fastify, les routes résident dans des plugins, et chaque plugin s’exécute dans son propre contexte d’encapsulation. Un décorateur, un hook ou une route enregistrés dans un plugin sont visibles par ce plugin et ses enfants, mais invisibles pour ses voisins — à l’opposé du middleware Express, où tout partage un seul objet de requête global. La référence Plugins documente ce modèle de portée.
Un plugin est simplement une fonction async qui reçoit l’instance encapsulée :
// routes/posts.js
export default async function postsRoutes(app, opts) {
app.get('/', async (request, reply) => {
return [] // le handler de liste viendra plus tard
})
}
Enregistrez-le sous un préfixe dans server.js :
import postsRoutes from './routes/posts.js'
app.register(postsRoutes, { prefix: '/api/posts' })
Maintenez les fonctions de plugin systématiquement en async. Le guide de migration v5 précise explicitement que toutes les dépréciations de la v4 ont été supprimées et ne fonctionneront plus après la mise à niveau, et le mélange des styles callback et promise dans un même plugin — toléré en v4 — n’est plus autorisé. Choisissez async/await et restez-y.
Validation et sérialisation par schéma
Attachez un objet schema à n’importe quelle route et Fastify valide le body, les params, le querystring et les headers entrants, puis sérialise la charge utile sortante selon le schéma response. C’est la fonctionnalité signature du framework, documentée sous Validation and Serialization.
Une règle de la v5 s’applique à chaque schéma que vous écrivez : le raccourci a disparu. À partir de la v5, vous devez fournir un JSON Schema complet pour querystring, params, body et response, incluant chacun la propriété type, sinon la route ne sera pas validée — le guide de migration v5 documente la suppression de l’option jsonShortHand.
Définissez les schémas pour la ressource posts :
// schemas/posts.js
export const postResponse = {
type: 'object',
properties: {
id: { type: 'string' },
title: { type: 'string' },
content: { type: 'string' },
published: { type: 'boolean' },
createdAt: { type: 'string' }
}
}
export const createPostBody = {
type: 'object',
required: ['title', 'content'],
properties: {
title: { type: 'string', minLength: 1 },
content: { type: 'string', minLength: 1 },
published: { type: 'boolean', default: false }
}
}
export const idParams = {
type: 'object',
required: ['id'],
properties: { id: { type: 'string' } }
}
Le schéma response est la partie que la plupart des tutoriels omettent, et il justifie sa place à double titre. Il se compile en un sérialiseur dédié plus rapide que la stringification générique, et il agit comme une liste d’autorisation : toute propriété retournée par votre handler qui n’est pas déclarée dans le schéma est supprimée avant d’atteindre le client. Un passwordHash ou un userId interne qui se glisse dans un résultat de requête ne quitte jamais le serveur, car le sérialiseur ne sait tout simplement pas comment l’émettre.
L’approche TypeScript avec TypeBox
Si vous travaillez en TypeScript, la friction réside dans la synchronisation des schémas de validation et des définitions de types. Le type provider TypeBox les regroupe en une seule déclaration. Installez typebox comme dépendance pair requise et @fastify/type-provider-typebox :
npm i typebox @fastify/type-provider-typebox
Enregistrez le provider et déclarez chaque schéma une seule fois avec Type :
import Fastify from 'fastify'
import { Type, TypeBoxTypeProvider } from '@fastify/type-provider-typebox'
const app = Fastify().withTypeProvider<TypeBoxTypeProvider>()
app.post('/api/posts', {
schema: {
body: Type.Object({
title: Type.String({ minLength: 1 }),
content: Type.String({ minLength: 1 })
})
}
}, async (request) => {
// request.body est typé { title: string; content: string }
const { title, content } = request.body
return { title, content }
})
Vous importez Type et TypeBoxTypeProvider depuis @fastify/type-provider-typebox et appelez Fastify().withTypeProvider<TypeBoxTypeProvider>() ; les types des champs du body sont alors automatiquement inférés. Utilisez le package scopé — l’import @sinclair/typebox hérité affiché dans les anciens guides correspond au nom d’avant la v5. Un point subtil de la v5 à connaître : le guide de migration indique que les type providers ont été divisés en deux types distincts, ValidatorSchema et SerializerSchema, donc mettez à niveau le package provider en même temps que Fastify. Notez également que les types du provider ne se propagent pas globalement ; en usage encapsulé, on peut remapper le contexte pour utiliser un ou plusieurs providers, ce qui signifie que vous re-déclarez le provider dans chaque plugin qui en a besoin.
La couche base de données comme plugin décorateur
Connectez la base de données à l’instance Fastify via un décorateur, exposé à travers un plugin qui rompt délibérément l’encapsulation. Pour partager une ressource — un client de base de données — entre toutes les routes, encapsulez le plugin dans fastify-plugin afin que la décoration échappe à sa propre portée, puis attachez-la avec fastify.decorate('prisma', client). La référence Decorators documente ce pattern.
Ce tutoriel utilise Prisma (v7, actuellement 7.8.x), qui est sans Rust et exclusivement ESM — une combinaison idéale avec la configuration ESM ci-dessus. Installez-le, définissez le schéma et générez le client. Prisma 7 requiert également un adaptateur de driver pour créer le client, donc installez l’adaptateur SQLite aux côtés des packages principaux :
npm i @prisma/client@7 @prisma/adapter-better-sqlite3
npm i -D prisma@7
// prisma/schema.prisma
generator client {
provider = "prisma-client"
output = "../generated/prisma"
}
datasource db {
provider = "sqlite"
url = "file:./dev.db"
}
model Post {
id String @id @default(uuid())
title String
content String
published Boolean @default(false)
createdAt DateTime @default(now())
}
Prisma 7 configure son CLI via un fichier prisma.config.ts et génère le client vers le chemin output que vous avez défini ci-dessus ; exécutez npx prisma migrate dev pour créer la base de données et le client. Exposez-le ensuite comme décorateur. Notez qu’en Prisma 7, l’appel nu new PrismaClient() a été supprimé — vous devez construire le client avec un adaptateur de driver :
// plugins/prisma.js
import fp from 'fastify-plugin'
import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3'
import { PrismaClient } from '../generated/prisma/client.js'
export default fp(async (app) => {
const adapter = new PrismaBetterSqlite3({ url: 'file:./dev.db' })
const prisma = new PrismaClient({ adapter })
await prisma.$connect()
app.decorate('prisma', prisma)
app.addHook('onClose', async (instance) => {
await instance.prisma.$disconnect()
})
})
Le plugin étant encapsulé dans fp, app.prisma est désormais accessible depuis toutes les routes de l’application.
Vous préférez Postgres avec TypeORM ? Le pattern de décorateur est identique : connectez le DataSource, puis app.decorate('db', dataSource). Seule la couche de schéma change.
Handlers CRUD avec les codes de statut appropriés
Implémentez les cinq opérations comme routes dans le plugin posts, chacune portant son schéma. Retournez le bon code de statut pour chaque verbe :
201avec la ressource créée sur POST ;200sur les lectures ;204sans corps sur DELETE ; et404lorsqu’une recherche échoue.
Fastify définit le Content-Type et sérialise selon votre schéma de réponse automatiquement — consultez la Reply API.
// routes/posts.js
import { postResponse, createPostBody, idParams } from '../schemas/posts.js'
export default async function postsRoutes(app) {
// CREATE
app.post('/', {
schema: { body: createPostBody, response: { 201: postResponse } }
}, async (request, reply) => {
const post = await app.prisma.post.create({ data: request.body })
return reply.code(201).send(post)
})
// READ all
app.get('/', {
schema: { response: { 200: { type: 'array', items: postResponse } } }
}, async () => {
return app.prisma.post.findMany({ orderBy: { createdAt: 'desc' } })
})
// READ one
app.get('/:id', {
schema: { params: idParams, response: { 200: postResponse } }
}, async (request, reply) => {
const post = await app.prisma.post.findUnique({ where: { id: request.params.id } })
if (!post) return reply.code(404).send({ message: 'Post not found' })
return post
})
// UPDATE
app.put('/:id', {
schema: { params: idParams, body: createPostBody, response: { 200: postResponse } }
}, async (request, reply) => {
const exists = await app.prisma.post.findUnique({ where: { id: request.params.id } })
if (!exists) return reply.code(404).send({ message: 'Post not found' })
return app.prisma.post.update({ where: { id: request.params.id }, data: request.body })
})
// DELETE
app.delete('/:id', {
schema: { params: idParams }
}, async (request, reply) => {
const exists = await app.prisma.post.findUnique({ where: { id: request.params.id } })
if (!exists) return reply.code(404).send({ message: 'Post not found' })
await app.prisma.post.delete({ where: { id: request.params.id } })
return reply.code(204).send()
})
}
Un point de vigilance de la v5 concernant DELETE : en v4, Fastify autorisait les requêtes DELETE avec un en-tête Content-Type: application/json et un corps vide ; cela n’est plus autorisé en v5. N’envoyez pas d’en-tête Content-Type en l’absence de charge utile.
Gestion des erreurs, hooks et durcissement pour la production
Centralisez la gestion des erreurs avec setErrorHandler et interceptez le cycle de vie des requêtes avec des hooks — les deux points d’extension qui remplacent la chaîne de middleware d’Express. La référence Hooks liste le flux des requêtes dans l’ordre : onRequest → preParsing → preValidation → preHandler → handler → preSerialization → onSend → onResponse, avec onError qui se déclenche lorsqu’une exception est levée.
class NotFoundError extends Error {
constructor (message) { super(message); this.statusCode = 404 }
}
app.setErrorHandler((error, request, reply) => {
request.log.error(error)
const status = error.statusCode ?? 500
reply.code(status).send({
error: error.name,
message: status === 500 ? 'Internal Server Error' : error.message
})
})
// Exemple de hook preHandler : filtrer une route avant l'exécution du handler
app.addHook('preHandler', async (request) => {
request.log.info({ url: request.url }, 'incoming request')
})
Un échec de validation court-circuite avant même que votre handler ne s’exécute, retournant automatiquement un 400 — l’une des raisons pour lesquelles les schémas réduisent le code des handlers. Pour mesurer le temps dans un hook, utilisez reply.elapsedTime ; la méthode reply.getResponseTime() a été supprimée en v5, et vous devez utiliser reply.elapsedTime à la place.
À partir de là, la checklist de production consiste en l’installation de plugins, chacun étant un package scopé @fastify/* qui s’enregistre de la même manière que votre plugin de routes :
@fastify/jwtpour l’authentification ;@fastify/swaggerpour générer la documentation OpenAPI directement à partir de vos schémas de routes ;@fastify/rate-limitpour la limitation de débit ; et@fastify/corspour l’accès cross-origin.
Pour les flux d’authentification avancés et le déploiement, référez-vous à des guides dédiés — ce sont des sujets à part entière. Si vous souhaitez observer le comportement de ces endpoints sous trafic réel, associez l’API à un outil de surveillance des erreurs Node.js côté front-end.
Vous disposez désormais d’une API CRUD Fastify v5 fonctionnelle où les schémas valident les entrées, sérialisent les sorties et se documentent eux-mêmes, et où la base de données est partagée via un décorateur plutôt qu’un import global. La prochaine étape concrète : ajoutez @fastify/swagger, pointez-le vers les schémas que vous avez déjà écrits, et observez la spécification OpenAPI se générer d’elle-même — la preuve que dans Fastify, le schéma est la source de vérité, et non une réflexion après coup.
FAQ
Quelle est la différence entre fastify.register et fastify.decorate ?
La méthode register monte un plugin dans son propre contexte d'encapsulation, de sorte que les routes, hooks et décorateurs ajoutés à l'intérieur restent limités à ce plugin et à ses enfants. La méthode decorate attache une propriété ou méthode réutilisable directement sur l'instance Fastify, la requête ou la réponse. Vous utilisez généralement decorate pour exposer des ressources partagées comme un client de base de données, et encapsulez ce plugin dans fastify-plugin afin que la décoration échappe à l'encapsulation et soit disponible dans toute l'application.
Fastify est-il plus rapide qu'Express, et dans quelle mesure ?
Le propre benchmark de Fastify rapporte jusqu'à 76 000 requêtes par seconde ou plus, mais les mainteneurs précisent qu'il s'agit d'un test synthétique de type « hello world » mesurant la surcharge du framework, et non le débit en conditions réelles. L'avantage mécanique est concret : Fastify compile les JSON Schemas en fonctions de validation et de sérialisation spécialisées au démarrage, ce qui permet de produire une réponse en contournant la surcharge générique de JSON.stringify. Mesurez toujours les performances de votre propre application avant de citer un quelconque multiplicateur de vitesse.
Pourquoi mon schéma de route Fastify v5 échoue-t-il à la validation alors qu'il fonctionnait en v4 ?
À partir de la v5, le raccourci de schéma et l'option jsonShortHand ont été supprimés, donc chaque schéma querystring, params, body et response doit être un JSON Schema complet incluant explicitement une propriété type. Un schéma qui ne déclare que des propriétés sans type de niveau supérieur ne sera plus validé. Ajoutez type 'object' à chaque objet de schéma. Le guide de migration v5 documente cela comme un changement cassant délibéré par rapport au comportement de raccourci de la v4.
Puis-je utiliser Fastify avec TypeScript sans écrire les types deux fois ?
Oui. Installez typebox comme dépendance pair ainsi que le package scopé @fastify/type-provider-typebox, puis appelez Fastify().withTypeProvider avec le générique TypeBoxTypeProvider. Vous déclarez chaque schéma une seule fois avec Type, et Fastify en infère les types de requête et de réponse, éliminant tout décalage entre les règles de validation et les interfaces TypeScript. Importez Type depuis @fastify/type-provider-typebox, et non depuis le nom hérité @sinclair/typebox affiché dans les anciens guides, et re-déclarez le provider dans chaque plugin qui en a besoin.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k