Créer Une API GraphQL Réactive Avec Fastify
GraphQL permet de décrire précisément les données exposées par une API, tandis que Fastify fournit un serveur Node.js rapide, léger et extensible. En associant les deux technologies, une application peut proposer des requêtes typées, des mutations cohérentes et des mises à jour en temps réel à partir d’un même point d’entrée.
Les souscriptions GraphQL ajoutent une communication persistante entre le client et le serveur. Lorsqu’un événement survient, les clients abonnés reçoivent automatiquement les données concernées, sans devoir interroger périodiquement l’API. Cette approche convient aux notifications, aux tableaux de bord, aux discussions instantanées et au suivi d’opérations longues.
L’objectif consiste ici à construire une API Fastify avec Mercurius, le plugin GraphQL de l’écosystème Fastify. Les résolveurs seront regroupés dans une structure unifiée afin de centraliser la logique métier, tandis qu’un mécanisme de publication distribuera les événements aux souscriptions actives.
Préparer Le Serveur Fastify Et GraphQL
Commencez par créer un projet Node.js et installez les dépendances nécessaires :
npm init -y
npm install fastify mercurius graphql
Mercurius prend en charge l’analyse du schéma, l’exécution des requêtes, les mutations et les souscriptions. La configuration minimale repose sur une instance Fastify, une définition SDL et un objet de résolveurs. Le serveur reste ainsi compact, tout en conservant les possibilités de validation et d’extension de GraphQL.
import Fastify from 'fastify'
import mercurius from 'mercurius'
const app = Fastify({ logger: true })
app.register(mercurius, {
schema,
resolvers,
subscription: true,
graphiql: true
})
app.listen({ port: 3000, host: '0.0.0.0' })
L’option subscription: true active le transport nécessaire aux abonnements. En développement, graphiql: true fournit une interface pratique pour tester les requêtes et observer les événements. En production, cette interface doit être protégée ou désactivée.
Définir Un Schéma Orienté Événements
Le schéma décrit les opérations disponibles et le contenu exact des messages échangés. Un exemple simple peut gérer une liste de tâches et notifier les clients lorsqu’une tâche est ajoutée :
const schema = `#graphql
type Task {
id: ID!
title: String!
completed: Boolean!
}
type Query {
tasks: [Task!]!
}
type Mutation {
addTask(title: String!): Task!
}
type Subscription {
taskAdded: Task!
}
`
Le type Task représente le contrat public. Le champ addTask reçoit un titre obligatoire et renvoie la ressource créée. Le champ taskAdded ne demande aucun argument : il décrit un flux de nouvelles tâches transmis aux clients dès qu’une mutation déclenche l’événement.
Cette précision réduit les réponses inutiles et facilite la génération de types côté TypeScript. Elle permet aussi de distinguer clairement la lecture initiale, l’écriture et le flux temps réel. Pour les systèmes complexes, les types peuvent être répartis dans plusieurs fichiers SDL avant d’être assemblés au démarrage.
Centraliser Les Résolveurs Et La Logique Métier
Un résolveur traduit un champ GraphQL en opération applicative. Pour éviter de mélanger accès aux données, validation et transport, placez les règles métier dans un service partagé. Les résolveurs deviennent alors des adaptateurs courts, réutilisables depuis une requête, une mutation ou une souscription.
const tasks = []
let nextId = 1
const taskService = {
list() {
return tasks
},
create(title) {
const task = {
id: String(nextId++),
title,
completed: false
}
tasks.push(task)
return task
}
}
const resolvers = {
Query: {
tasks: () => taskService.list()
},
Mutation: {
addTask: async (_, { title }, { pubsub }) => {
const task = taskService.create(title)
await pubsub.publish({
topic: 'TASK_ADDED',
payload: { taskAdded: task }
})
return task
}
},
Subscription: {
taskAdded: {
subscribe: (_, __, { pubsub }) =>
pubsub.subscribe('TASK_ADDED')
}
}
}
Cette organisation forme un ensemble de résolveurs unifié : chaque domaine fonctionnel expose ses opérations sous les clés Query, Mutation et Subscription, tout en utilisant le même service. La logique de création n’est donc pas dupliquée dans la souscription. Dans une application réelle, remplacez le tableau en mémoire par un dépôt PostgreSQL, MongoDB ou Redis.
Les performances doivent être mesurées avec des données représentatives. Le temps passé dans une requête peut provenir de la base de données, d’un appel distant ou d’un traitement algorithmique ; un rappel sur la complexité des tris aide à repérer les choix coûteux lorsqu’un résolveur trie de nombreux résultats.
Diffuser Les Souscriptions En Temps Réel
Mercurius fournit un mécanisme de publication adapté aux petites architectures et aux déploiements sur une seule instance. Le client ouvre une connexion persistante, puis s’abonne à un topic. Chaque appel à pubsub.publish transmet la charge utile aux connexions qui écoutent ce topic.
Côté navigateur, un client GraphQL peut envoyer une souscription telle que :
subscription {
taskAdded {
id
title
completed
}
}
Pour des besoins d’intégration, l’URL GraphQL peut être documentée dans une zone d’intégration avec les exemples de requêtes, les variables et les événements disponibles. Dans un environnement multi-instance, le bus local doit être remplacé par Redis, NATS ou Kafka afin qu’un événement publié par un nœud atteigne les clients connectés à un autre.
Il est également utile de filtrer les événements par utilisateur, équipe ou projet. Le résolveur de souscription peut vérifier le contexte d’authentification avant d’écouter un topic, puis appliquer une fonction de filtrage lorsque seuls certains événements doivent être transmis.
Sécuriser Les Résolveurs Et Le Contexte
Le contexte GraphQL est l’endroit approprié pour fournir l’utilisateur authentifié, un client de base de données, un service de journalisation ou une instance de publication. Le contexte est créé pour chaque opération et ne doit pas contenir de secrets exposés au navigateur.
app.register(mercurius, {
schema,
resolvers,
subscription: true,
context: async (request, reply) => ({
user: await authenticate(request),
requestId: request.id
})
})
Chaque mutation doit contrôler les droits avant de modifier les données ou de publier un événement. Ajoutez également une limitation de profondeur et de complexité des requêtes afin d’éviter les requêtes GraphQL excessivement imbriquées. Les erreurs internes doivent être journalisées côté serveur, sans révéler les détails de la base de données au client.
Les paramètres de connexion, les clés de signature et les URL de services doivent rester dans des variables d’environnement ou un gestionnaire de secrets. Un comparatif Consul et etcd permet de choisir une stratégie cohérente selon le nombre d’instances, les besoins de rotation et l’infrastructure utilisée.
Tester Et Déployer Une API Réactive
Les tests doivent couvrir les résolveurs isolément, les requêtes GraphQL complètes et le cycle de vie d’une souscription. Vérifiez qu’un utilisateur non autorisé ne reçoit aucun événement, qu’une mutation publie une seule notification et qu’une déconnexion libère correctement les ressources.
Prévoyez aussi des contrôles opérationnels : métriques sur le nombre de connexions actives, durée des résolveurs, erreurs de publication et taille des charges utiles. Un service tiers peut parfois servir de cas de test pour les appels distants, à condition d’en isoler les effets ; documentez alors clairement le service externe utilisé et ses limites.
Voici une comparaison rapide des options de transport et de diffusion :
| Solution | Usage principal | Avantage | Point de vigilance |
|---|---|---|---|
| PubSub Mercurius | Prototype ou instance unique | Configuration minimale | Événements locaux |
| Redis Pub/Sub | Plusieurs instances Fastify | Mise en place accessible | Persistance limitée |
| NATS | Flux distribués rapides | Faible latence et scalabilité | Infrastructure supplémentaire |
| Kafka | Événements durables et nombreux | Relecture et conservation | Administration plus complexe |
Pour un déploiement fiable, conteneurisez l’application, placez-la derrière un proxy compatible avec les connexions persistantes et définissez des délais d’inactivité adaptés. Les recommandations suivantes réduisent les erreurs courantes :
- Séparer le schéma GraphQL, les services métier et les adaptateurs de données.
- Valider les arguments dans les mutations avant tout accès à la base.
- Authentifier les connexions avant d’autoriser une souscription.
- Utiliser un bus partagé dès que plusieurs instances sont déployées.
- Limiter la profondeur, la complexité et la taille des requêtes.
- Surveiller les connexions ouvertes et les temps d’exécution des résolveurs.
Une API GraphQL Fastify bien structurée associe un contrat explicite, des résolveurs courts et un système d’événements maîtrisé. Commencez avec Mercurius et un PubSub local, puis faites évoluer le stockage, l’authentification et le bus de messages selon les contraintes réelles de votre application. Pour signaler un problème d’intégration ou partager un cas d’usage, utilisez la page de contact.