Introduction
En 2026, GraphQL reste le standard pour les APIs flexibles et performantes, surpassant REST dans les apps complexes comme les dashboards ou les apps mobiles. Ce tutoriel avancé vous guide pour implémenter un serveur GraphQL complet avec Apollo Server 4, TypeScript, Prisma pour l'ORM et DataLoader pour le batching et l'évitement des problèmes N+1. Nous construirons un blog CRUD (Users/Posts) avec authentification JWT, contexte partagé, resolvers optimisés et gestion d'erreurs pro.
Pourquoi c'est crucial ? Les resolvers naïfs génèrent des requêtes en cascade inefficaces ; DataLoader les regroupe intelligemment, comme un livreur qui consolide les colis avant livraison. Résultat : scalabilité x10. Ce guide est 100% actionnable : copiez-collez le code, lancez en 10 min. Idéal pour seniors voulant maîtriser les patterns production-ready.
Prérequis
- Node.js 20+ et npm/yarn
- Connaissances avancées en TypeScript et GraphQL (schéma, resolvers)
- Bases de Prisma et bases de données relationnelles
- Éditeur avec support TS (VS Code recommandé)
- Outils : GraphQL Playground ou Altair pour tester
Initialisation du projet
mkdir graphql-advanced && cd graphql-advanced
npm init -y
npm install @apollo/server graphql @prisma/client prisma express cors dotenv jsonwebtoken
dnpm install -D typescript @types/node @types/express @types/jsonwebtoken tsx
npx tsc --init --target es2022 --module commonjs --lib es2022,dom --outDir dist --rootDir src --strict --esModuleInterop --skipLibCheck
mkdir -p src/{graphql,lib,prisma/migrations}Ce script initialise un projet Node.js avec TypeScript, installe Apollo Server pour GraphQL, Prisma pour l'ORM/SQLite, et les dépendances pour auth (JWT) et serveur HTTP. tsconfig.json est configuré pour ES2022 strict, évitant les pièges de compatibilité. Lancez-le pour un setup zéro-config.
Configuration TypeScript et .env
Créez tsconfig.json via le script ci-dessus et ajustez-le si besoin. Ajoutez un fichier .env à la racine : DATABASE_URL="file:./dev.db". Cela prépare Prisma pour une DB SQLite embarquée, parfaite pour dev/test sans Docker.
Schéma Prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
name String
email String @unique
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
content String
author User @relation(fields: [authorId], references: [id])
authorId Int
}Ce schéma définit deux modèles relationnels : User (1:N) Post. L'annotation @relation gère les foreign keys automatiquement. SQLite simplifie les tests ; en prod, passez à PostgreSQL sans changer le code client.
Génération et migration DB
npx prisma generate
npx prisma db pushGénère le client Prisma TypeScript et synchronise la DB avec le schéma (équivalent migrate dev). db push est idéal pour proto ; utilisez migrate dev en prod pour versioning. Vérifiez avec npx prisma studio.
Préparation des types GraphQL
Nous adoptons l'approche schema-first avec typeDefs en template literal pour la lisibilité, complétés par des resolvers TS. Créez le dossier src/graphql/ pour organiser.
Définition du schéma GraphQL
import { gql } from 'graphql-tag';
export const typeDefs = gql`
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
}
type Query {
users: [User!]!
user(id: ID!): User
posts: [Post!]!
post(id: ID!): Post
postsByUser(id: ID!): [Post!]!
}
type Mutation {
createUser(name: String!, email: String!): User!
createPost(title: String!, content: String!, authorId: ID!): Post!
}
type Subscription {
postAdded: Post!
}
schema {
query: Query
mutation: Mutation
subscription: Subscription
}
`;Ce schéma expose queries CRUD, mutations et une subscription basique. Les types non-null (!) forcent la validation. postsByUser testera DataLoader pour N+1. Subscriptions prêtes pour WebSocket.
DataLoaders pour batching
import DataLoader from 'dataloader';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export interface Context {
prisma: PrismaClient;
loaders: {
postsByUserId: DataLoader<number, any[]>;
};
}
export const createLoaders = () => ({
postsByUserId: new DataLoader(async (userIds: number[]) => {
const posts = await prisma.post.findMany({
where: { authorId: { in: userIds as number[] } },
include: { author: true },
});
const postMap = new Map();
posts.forEach((post) => {
if (!postMap.has(post.authorId)) postMap.set(post.authorId, []);
(postMap.get(post.authorId) as any[]).push(post);
});
return userIds.map((id) => postMap.get(id) || []);
}),
});
export type LoaderKeys = keyof ReturnType<typeof createLoaders>;DataLoader batch les requêtes postsByUser en une seule DB query, évitant N+1 (ex: 10 users → 10 queries devient 1). Map() groupe par authorId. Injecté dans Context pour resolvers.
Contexte avec authentification JWT
import { Request } from 'express';
import jwt from 'jsonwebtoken';
import { PrismaClient } from '@prisma/client';
import { createLoaders, Context, LoaderKeys } from './dataloaders';
const prisma = new PrismaClient();
const SECRET = process.env.JWT_SECRET || 'secret';
export const createContext = ({ req }: { req: Request }): Context => {
const token = req.headers.authorization?.replace('Bearer ', '');
let userId = -1;
if (token) {
try {
const decoded = jwt.verify(token, SECRET) as { userId: number };
userId = decoded.userId;
} catch {}
}
return {
prisma,
userId,
loaders: createLoaders() as any,
};
};Le contexte partage Prisma, userId décodé du JWT (via header Bearer) et DataLoaders. Vérif token protège mutations. userId accessible dans resolvers pour auth fine-grained. SECRET en .env.
Implémentation des resolvers
Analogie : Les resolvers sont comme des fonctions de vue en SQL – ils résolvent les champs leaf. Ici, on optimise posts avec DataLoader et protège les mutations.
Resolvers optimisés
import { IResolvers } from '@graphql-tools/utils';
import { Context } from '../lib/dataloaders';
import { GraphQLError } from 'graphql';
export const resolvers: IResolvers<any, Context> = {
Query: {
users: async (_, __, { prisma }) => prisma.user.findMany({ include: { posts: true } }),
user: async (_, { id }, { prisma }) => {
const user = await prisma.user.findUnique({ where: { id: Number(id) }, include: { posts: true } });
if (!user) throw new GraphQLError('User not found');
return user;
},
posts: async (_, __, { prisma }) => prisma.post.findMany({ include: { author: true } }),
post: async (_, { id }, { prisma }) => {
const post = await prisma.post.findUnique({ where: { id: Number(id) }, include: { author: true } });
if (!post) throw new GraphQLError('Post not found');
return post;
},
postsByUser: async (_, { id }, { loaders }) =>
loaders.postsByUserId.load(Number(id)),
},
Mutation: {
createUser: async (_, { name, email }, { prisma }) =>
prisma.user.create({ data: { name, email } }),
createPost: async (_, { title, content, authorId }, { prisma, userId }) => {
if (userId === -1) throw new GraphQLError('Unauthorized');
return prisma.post.create({
data: { title, content, authorId: Number(authorId) },
include: { author: true },
});
},
},
User: {
posts: ({ id }, _, { loaders }) => loaders.postsByUserId.load(id),
},
};Resolvers Query/Mutation gèrent CRUD avec includes Prisma pour jointures. postsByUser et champ User.posts utilisent DataLoader pour batching. Mutations check userId du context ; erreurs custom avec GraphQLError pour UX pro.
Lancement du serveur Apollo
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { expressMiddleware } from '@apollo/server/express4';
import express from 'express';
import cors from 'cors';
import { typeDefs } from './graphql/schema.js';
import { resolvers } from './graphql/resolvers.js';
import { createContext } from './lib/context.js';
const app = express();
const server = new ApolloServer({
typeDefs,
resolvers,
});
await server.start();
app.use(
'/',
cors(),
express.json(),
expressMiddleware(server, {
context: async ({ req }) => createContext({ req }),
}),
);
const { url } = await startStandaloneServer(server, { listen: { port: 4000 } });
console.log(`🚀 Server ready at ${url}`);
// Pour Express: app.listen(4000); mais standalone pour subscriptions
export default app;ApolloServer intègre typeDefs/resolvers avec context custom. Middleware Express gère CORS/JSON ; startStandaloneServer active subscriptions WebSocket. Accédez à http://localhost:4000 pour Playground.
Test et exécution
Ajoutez à package.json : "dev": "tsx watch src/server.ts". Lancez npm run dev. Testez queries dans Playground : query { users { name posts { title } } } – observez DataLoader en logs Prisma.
Bonnes pratiques
- Toujours batcher avec DataLoader pour relations 1:N, même en prod.
- Injectez Prisma dans context (singleton) pour pooling connexions.
- Validez inputs avec class-validator dans mutations.
- Utilisez persisted queries et Apollo Gateway pour federation multi-svc.
- Monitorez avec Apollo Studio pour tracing perf.
Erreurs courantes à éviter
- N+1 queries : Résolvez avec DataLoader, pas includes Prisma partout (overfetch).
- Context sans async/await : JWT decode peut fail, wrappez en try/catch.
- Pas de rate limiting : Ajoutez express-rate-limit pour DDoS.
- Subscriptions sans WebSocket : StandaloneServer les gère ; testez avec ws://.
Pour aller plus loin
- Docs Apollo Server 4 : apollo.dev
- Prisma Accelerate pour caching global
- GraphQL Federation pour microservices
- Découvrez nos formations Learni sur GraphQL avancé pour masterclasses live.