Créer une API GraphQL sécurisée en termes de types avec Prisma et Nexus dans Node.js
Suivez une procédure en sept étapes pour créer une API GraphQL Node.js qui unifie le modèle de données Prisma avec les types et résolveurs générés par Nexus.
Découvrez comment intégrer Prisma Nexus dans un projet Node.js pour créer des API GraphQL sécurisées du point de vue des types, en abordant la conception du schéma, la logique des résolveurs et le fonctionnement d’un serveur.
Imaginez un projet GraphQL où le même type « User » est défini en quatre endroits différents : un document SDL, une interface TypeScript écrite manuellement, un modèle Prisma, ainsi qu’un validateur Zod ajouté par un collègue des mois après le lancement. Chaque fois qu’une de ces définitions change, au moins une des autres cesse d’être synchronisée. Une correction est publiée, et soudainement les types TypeScript continuent de considérer que phone est obligatoire, alors même que cette colonne a disparu de la base de données des semaines auparavant.
C’est précisément ce genre d’écart que la combinaison Prisma et Nexus est conçue pour éviter. Nexus génère votre schéma GraphQL ainsi que vos types TypeScript directement à partir du même modèle de données que vous avez déjà défini dans Prisma. Il existe une seule source de vérité, et tout le reste est dérivé d’elle. En mettant à jour la définition une seule fois, les types, le schéma et les signatures des résolveurs évoluent en même temps. Cela semble logique une fois exprimé à voix haute — la véritable leçon vient du fait de travailler sans cela et de constater à quel point cette lacune devient coûteuse.
Ce guide vous accompagne dans la création d’une API GraphQL Node.js à partir de zéro, en utilisant Prisma et Nexus. Il est divisé en sept étapes, avec du code complet, sans rien omettre. À la fin, vous disposerez d’un serveur fonctionnel connecté à PostgreSQL — quelque chose que vous pouvez exécuter, développer et comprendre en toute confiance. Il s’agit d’une base suffisamment solide pour un vrai backend e-commerce en production, et non d’une démonstration qui tombe en panne dès l’ajout d’un deuxième modèle.
Prérequis avant l’étape 1
- Node.js installé — téléchargez la dernière version LTS depuis nodejs.org si ce n’est pas déjà fait.
- La CLI Prisma disponible globalement :
npm install -g prisma
- Une base de données PostgreSQL en cours d’exécution à laquelle vous pouvez accéder. Un conteneur Docker local, le plan gratuit de Supabase, Railway — l’hébergement n’a pas d’importance, tant que vous disposez d’une chaîne de connexion.
Une remarque pour ceux qui appliquent cela à une base de code existante plutôt qu’à un projet neuf : lors de la première migration, Prisma tente de concilier schema.prisma avec ce qui existe déjà dans la base de données. Avec un schéma hérité désordonné, cette étape de conciliation peut générer une différence importante et intimidante. Lisez-la attentivement avant de l’appliquer, et testez toujours d’abord dans un environnement de développement. Si vous commencez à zéro, rien de tout cela ne s’applique pour l’instant.
Étape 1 : Mettre le projet en marche
C’est l’étape la plus rapide de tout le processus. Créez un dossier et téléchargez toutes les dépendances en une seule fois :
mkdir prisma-nexus-graphql
cd prisma-nexus-graphql
# Initialize your project
npm init -y# Install required dependencies
npm install graphql nexus prisma express apollo-server-express path
Cette seule commande installe en même temps les sept packages : le runtime GraphQL, Nexus pour la création de schémas basés sur du code, Prisma lui-même, ainsi que le couple Apollo/Express qui exécutera le serveur. Installer tout cela ensemble n’est pas seulement une question de commodité — cela permet à npm de résoudre les dépendances croisées entre tous ces packages en une seule étape, évitant ainsi le risque de versions mineures incompatibles si l’on installait les packages un par un.
Étape 2 : Connecter Prisma à votre base de données
npx prisma init
Répondez aux questions et choisissez PostgreSQL. Une fois la commande terminée, deux nouveaux fichiers apparaissent qui n’étaient pas là auparavant :
prisma/schema.prisma— c’est ici que se trouve votre modèle de données.env— c’est ici que doit être placée la chaîne de connexionDATABASE_URL, et elle doit y être mise immédiatement
Ce n’est pas une exagération. Avant de toucher au schéma, avant d’exécuter une migration, avant d’ouvrir quoi que ce soit d’autre, mettez votre chaîne de connexion dans .env. Désormais, presque tous les commandes Prisma tentent de se connecter à la base de données, et les erreurs qui apparaissent en l’absence ou en cas de format incorrect de cette chaîne sont particulièrement peu utiles. Au lieu d’un message clair indiquant « chaîne de connexion invalide », vous obtiendrez une description vague concernant le fait que le client n’a pas été initialisé — et vous risquez facilement de perdre quinze minutes à chercher la cause erronée.
Étape 3 : Écrire le schéma Prisma — ce n’est pas votre schéma GraphQL
Si vous avez déjà travaillé avec GraphQL mais jamais en conjonction avec Prisma, évitez de considérer schema.prisma comme l’endroit où vous concevez l’interface de votre API. Ce n’est pas le cas : il s’agit d’une représentation de la structure de votre base de données — tables, colonnes, relations, contraintes. La forme réelle de l’API est dérivée ultérieurement à partir de celui-ci, via Nexus. Gardez cette distinction en tête, car elle permet de maintenir la cohérence du modèle mental global.
// schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}model User {
id Int @id @default(autoincrement())
name String
email String @unique
}
Lorsque votre modèle est écrit, exécutez la migration :
npx prisma migrate dev
This single command does two things nothing else in the setup does: it creates the actual table in your database, and it regenerates Prisma Client with TypeScript types that exactly match your current schema. Skip it, and Prisma Client simply won't recognize that a User model exists. What you get instead are type errors buried in generated files you don't control, with call stacks that lead nowhere useful — there's no clever shortcut around that. Run the migration every time your schema changes, without exception.
Step 4: Nexus — Why One More File Is Worth It
À ce stade de la configuration, il est légitime de se demander si Nexus remplit réellement sa fonction. Rien ne vous empêche de créer un serveur GraphQL sans lui : écrivez manuellement le SDL, définites vos interfaces TypeScript vous-même et connectez tout aux résolveurs de manière manuelle. De nombreux projets le font effectivement. Le problème, c’est que cette approche ouvre la porte à un type particulier de bug : le SDL décrit une forme donnée, les types TypeScript en décrivent une légèrement différente, tandis que le résolveur renvoie quelque chose de complètement autre. Déterminer quelle des trois versions est la « vraie » prend souvent plus de temps que la création initiale de cette fonctionnalité elle-même.
Nexus contourne ce problème en traitant le SDL comme un résultat généré plutôt que quelque chose que l’on écrit manuellement. Vous décrivez vos types en TypeScript, et Nexus dérive à la fois le SDL ainsi que les définitions de types correspondantes à partir de cette seule source. Les trois éléments qui étaient auparavant séparés forment un seul artefact qui ne peut pas être en contradiction avec lui-même sur le plan structurel. Voici à quoi ressemble schema.ts :
// schema.ts
import { makeSchema } from 'nexus';
import path from 'path';
import * as resolvers from './resolvers';const schema = makeSchema({
types: [resolvers],
outputs: {
schema: path.join(__dirname, './generated/schema.graphql'),
typegen: path.join(__dirname, './generated/nexus.ts'),
},
});export default schema;
La configuration outputs indique à Nexus où placer les fichiers qu’il génère : generated/schema.graphql contient le SDL, tandis que generated/nexus.ts contient les définitions TypeScript correspondantes. Ces fichiers sont réécrits à chaque exécution, il ne faut donc jamais les modifier manuellement. Si vous ouvrez generated/nexus.ts et que vous remarquez quelque chose à corriger, résistez à l’envie de le modifier directement — localisez plutôt la définition source et modifiez-la là. Modifier un fichier généré, c’est un peu comme appliquer des correctifs à un binaire compilé : cela fonctionne tant que la prochaine compilation n’efface pas silencieusement vos modifications.
Étape 5 : Résolveurs — Relier le schéma à la base de données
// resolvers.ts
import { extendType, stringArg, nonNull, objectType } from 'nexus';
import { PrismaClient } from '@prisma/client';const prisma = new PrismaClient();export const User = objectType({
name: 'User',
definition(t) {
t.nonNull.id('id')
t.string('name')
t.string('email')
},
})export const Query = extendType({
type: 'Query',
definition(t) {
t.list.field('users', {
type: 'User',
resolve: async () => {
return await prisma.user.findMany();
},
});
},
});export const Mutation = extendType({
type: 'Mutation',
definition(t) {
t.field('createUser', {
type: 'User',
args: {
name: nonNull(stringArg()),
email: nonNull(stringArg()),
},
resolve: async (_, args) => {
return await prisma.user.create({
data: {
name: args.name,
email: args.email,
},
});
},
});
},
});
Remarquez que PrismaClient est instancié une seule fois, au niveau le plus élevé du module, en dehors de tout corps de fonction. Ce placement est plus important qu’il n’y paraît au premier abord. Chaque appel à new PrismaClient() ouvre une nouvelle connexion au database. Si vous le créiez à l’intérieur d’un résolveur, une nouvelle connexion serait établie à chaque requête. Lors du développement local normal, avec peut-être une ou deux requêtes par seconde, le database ne remarquera même pas la différence. Mais en cas de trafic concurrentiel réel — imaginez quelques centaines d’acheteurs accédant à /checkout en même temps pendant une vente — ce schéma épuisera la limite de connexions de PostgreSQL et commencera à générer des erreurs sous charge.
Déclarer le client au niveau du module signifie que l’ensemble du processus partage une seule connexion. Les requêtes ne tentent pas d’ouvrir leurs propres connexions de base de données ; elles s’inscrivent dans une file d’attente relative à un client partagé, qui gère internement son propre pool de connexions. Ce sont ce genre de détails que les développeurs expérimentés en Node.js appliquent instinctivement, tandis que les équipes moins expérimentées ont tendance à les découvrir de la manière difficile, au milieu d’un incident. Vous pouvez maintenant éviter cette leçon.
Étape 6 : Le serveur
// server.ts
import express from 'express';
import { ApolloServer } from 'apollo-server-express';
import schema from './schema';const app = express();
const server = new ApolloServer({ schema });const startServer = async () => {
await server.start(); // Start Apollo Server server.applyMiddleware({ app }); // Apply Apollo Server middleware to Express const PORT = process.env.PORT || 4000; app.listen(PORT, () => {
console.log(`Server is running at http://localhost:${PORT}/graphql`);
});
}startServer().catch((err) => {
console.error('Error starting the server:', err);
});
Un détail important à noter avant de commencer : await server.start() doit être exécuté avant server.applyMiddleware(). Cette exigence d’ordre n’existait pas dans Apollo Server 2 — Apollo 3 a introduit une phase de démarrage asynchrone explicite, et tout code d’exemple écrit avant la fin de 2021 manque très probablement complètement de cette appel. Si vous l’omettez, vous obtiendrez l’erreur Server must be started before calling server.applyMiddleware, ce qui est au moins clair sur le problème, même s’il n’explique pas pourquoi cette règle existe. Une fois que vous comprenez le raisonnement, c’est une correction en deux secondes plutôt qu’un détour confus.
Étape 7 : Le démarrer. Le briser. Y faire confiance.
node server.ts
Accédez à http://localhost:4000/graphql. Cela vous amène dans le GraphQL Playground. Exécutez d’abord la mutation :
// Fetch Users
query {
users {
id
name
email
}
}
// Create Users
mutation {
createUser(name: "John Doe", email: "john@example.com") {
id
name
email
}
}
Exécutez la mutation avant la requête, afin qu’il y ait réellement des données à récupérer. Observez comment l’enregistrement que vous venez d’insérer apparaît dans la réponse de la requête. Ensuite, faites ce que la plupart des guides omettent : ouvrez un client de base de données — psql, TablePlus, DBeaver, quel que soit l’outil que vous utilisez — et examinez directement la table User. Pas le JSON retourné par l’API, mais la table brute elle-même.
Votre ligne se trouve bien là, créée par une mutation GraphQL que vous avez définie en TypeScript à l’aide de types Nexus, exécutée via Prisma, et persistée dans PostgreSQL. Chaque maillon de cette chaîne a fonctionné correctement. Vous pouvez ainsi identifier l’endroit exact où le code de votre application interagit avec la base de données. Pour ceux qui viennent d’une longue expérience avec des endpoints REST et du SQL manuel, c’est généralement le moment où cette stack cesse de ressembler à un schéma pour devenir quelque chose de concret.
Ce que vous avez construit et ce qu’il vous reste à ajouter
Ce que vous avez maintenant, c’est une base backend fonctionnelle, et non un exemple de démonstration. Le schéma que vous venez d’appliquer — définir un modèle Prisma, exécuter une migration, ajouter un objectType Nexus, écrire le résolveur, et l’intégrer au serveur Apollo/Express — est exactement ce que vous ferez pour chaque modèle supplémentaire que vous introduirez. Que ce soit Product, Order ou Cart, les étapes restent identiques, tout comme les garanties offertes. Ajoutez une relation dans schema.prisma, exécutez migrate dev, puis implémentez le résolveur : vos types se mettront à jour automatiquement. Cette synchronisation automatique constitue véritablement l’avantage principal de cette configuration — vous n’avez plus besoin de compter sur la mémoire pour maintenir en cohérence votre schéma, vos types et vos résolveurs, car les outils s’en chargent à votre place.
Ce qui manque de manière évidente pour l’instant, ce sont l’authentification, l’autorisation, la limitation des fréquences et la validation des entrées. Nexus garantit que vos types sont corrects, mais il ne précise pas qui a le droit d’appeler telle ou telle opération. Actuellement, la mutation createUser répondra volontiers à quiconque parvient au port 4000. Cela est acceptable pendant le développement local, mais cela devient inacceptable dès que l’API est accessible via une URL réelle. Il faut ajouter un middleware d’authentification avant que cela ne soit mis en production dans un environnement accessible aux autres.
Pour une couverture plus approfondie, consultez la documentation Prisma concernant les relations, le filtrage et la pagination, ainsi que la documentation Nexus sur l’autorisation au niveau des champs et les scalaires personnalisés. Ces deux ensembles de documents sont suffisamment bien organisés pour être lus du début à la fin, plutôt que de n’être consultés que rapidement en cas de problème — une caractéristique bien plus rare qu’elle ne le devrait dans la documentation technique.
Lectures complémentaires
- Création d’agents IA sécurisés avec LangChain Guardrails et Middleware — Découvrez comment les mécanismes déterministes et basés sur des modèles fonctionnent dans LangChain pour détecter les fuites de PII, appliquer des règles métier et intégrer des étapes d’approbation humaine dans les agents IA.