Introduction
GraphQL est le standard pour les APIs modernes en 2026, surpassant REST par sa flexibilité et son efficacité. Ce tutoriel avancé vous guide dans l'implémentation d'une API complète avec Apollo Server 4, Prisma 6 pour l'ORM, authentification JWT, batching de requêtes, caching et subscriptions WebSocket pour du real-time. Imaginez une app de blog avec users, posts et commentaires : clients demandent exactement les données nécessaires, sans sur-fetching.
Pourquoi c'est crucial ? Les APIs GraphQL réduisent la latence de 40-60% en production, gèrent la complexité N+1 via DataLoader, et scalent avec federation. Nous construirons un serveur TypeScript production-ready, testable et sécurisé. À la fin, vous déployez sur Vercel ou un cluster Kubernetes. Prêt à booster vos backends ? (128 mots)
Prérequis
- Node.js 20+ et npm 10+
- Connaissances avancées en TypeScript et async/await
- Familiarité avec Prisma et bases de données relationnelles
- Outils : GraphQL Playground ou Apollo Studio pour tester
Initialisation du projet
mkdir graphql-advanced-api && cd graphql-advanced-api
npm init -y
npm install @apollo/server @apollo/server-plugin-drain-http-server graphql prisma @prisma/client jsonwebtoken bcryptjs @types/jsonwebtoken @types/bcryptjs @types/node
databases/sqlite
docker run --name prisma-db -e POSTGRES_PASSWORD=prisma -p 5432:5432 -d postgres
npm install -D typescript ts-node @types/express express cors
npx prisma init --datasource-provider sqliteCe script initialise un projet Node.js avec Apollo Server, Prisma (SQLite pour simplicité, portable), JWT pour auth et dépendances TypeScript. On lance un conteneur Postgres optionnel ; ici SQLite pour standalone. Évitez les DB cloud en dev pour la reproductibilité.
Configuration de Prisma
Prisma génère un client type-safe pour notre schéma : Users, Posts, Comments avec relations. On définit les modèles avancés avec indexes et uniques pour scaler.
Schéma Prisma complet
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = "file:./dev.db"
}
model User {
id String @id @default(uuid())
email String @unique
password String
name String?
posts Post[]
createdAt DateTime @default(now())
}
model Post {
id String @id @default(uuid())
title String
content String
authorId String
author User @relation(fields: [authorId], references: [id])
comments Comment[]
createdAt DateTime @default(now())
@@index([authorId])
}
model Comment {
id String @id @default(uuid())
content String
postId String
post Post @relation(fields: [postId], references: [id])
createdAt DateTime @default(now())
@@index([postId])
}Schéma relationnel complet avec UUID, indexes pour perf et timestamps. SQLite pour dev rapide ; migrez vers Postgres en prod. Exécutez npx prisma migrate dev --name init puis npx prisma generate après.
TypeDefs GraphQL avancés
import { gql } from 'graphql-tag';
export const typeDefs = gql`
type User {
id: ID!
email: String!
name: String
posts: [Post!]!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
comments: [Comment!]!
}
type Comment {
id: ID!
content: String!
post: Post!
}
type Query {
me: User
posts: [Post!]!
post(id: ID!): Post
}
type Mutation {
createPost(title: String!, content: String!): Post!
deletePost(id: ID!): Boolean!
addComment(postId: ID!, content: String!): Comment!
}
type Subscription {
postAdded: Post!
}
scalar DateTime
`; TypeDefs stricts avec relations scalaires et subscriptions. Pas de N+1 grâce aux resolvers. Ajoutez DateTime scalar pour timestamps Prisma.
Resolvers avec DataLoader et auth
Les resolvers gèrent la logique métier : batching anti-N+1 via DataLoader, auth JWT dans context, hashing bcrypt.
Utils JWT et bcrypt
import jwt from 'jsonwebtoken';
import bcrypt from 'bcryptjs';
const JWT_SECRET = process.env.JWT_SECRET || 'supersecretdevkey2026';
export const hashPassword = async (password: string): Promise<string> => {
return bcrypt.hash(password, 12);
};
export const verifyPassword = async (password: string, hash: string): Promise<boolean> => {
return bcrypt.compare(password, hash);
};
export const createToken = (payload: { userId: string }): string => {
return jwt.sign(payload, JWT_SECRET, { expiresIn: '1h' });
};
export const verifyToken = (token: string): { userId: string } | null => {
try {
return jwt.verify(token, JWT_SECRET) as { userId: string };
} catch {
return null;
}
};Fonctions utilitaires sécurisées pour JWT (expiresIn court) et bcrypt (salt 12). Env JWT_SECRET en prod via dotenv. Piège : ne jamais commiter les secrets.
Context avec auth et DataLoader
import { Request } from 'express';
import DataLoader from 'dataloader';
import { PrismaClient } from '@prisma/client';
import { verifyToken } from './utils/auth';
const prisma = new PrismaClient();
export interface Context {
prisma: PrismaClient;
userId: string | null;
loaders: {
users: DataLoader<string, any>;
postsByUser: DataLoader<string, any[]>;
};
}
export const createContext = ({ req }: { req: Request }): Context => {
const token = req.headers.authorization?.replace('Bearer ', '') || '';
const userId = verifyToken(token)?.userId || null;
return {
prisma,
userId,
loaders: {
users: new DataLoader(async (ids) => {/* impl batch */}),
postsByUser: new DataLoader(async (userIds) => {/* batch posts */}),
},
};
};Context avancé injecte Prisma, user auth et DataLoaders pour batching. Implémentez les batch functions dans resolvers. Évite les pièges de mémoire : disposez Prisma en shutdown.
Resolvers complets avec batching
import { Resolvers } from './types'; // généré par codegen
import { Context } from './context';
export const resolvers: Resolvers<Context> = {
Query: {
me: async (_, __, { userId, prisma }) => {
if (!userId) throw new Error('Non authentifié');
return prisma.user.findUnique({ where: { id: userId }, include: { posts: true } });
},
posts: async (_, __, { prisma }) => {
return prisma.post.findMany({ include: { author: true, comments: true } });
},
post: async (_, { id }, { prisma }) => {
return prisma.post.findUnique({ where: { id }, include: { author: true, comments: true } });
},
},
Post: {
author: async (parent, _, { loaders }) => {
return loaders.users.load(parent.authorId);
},
},
Mutation: {
createPost: async (_, { title, content }, { userId, prisma }) => {
if (!userId) throw new Error('Auth required');
return prisma.post.create({ data: { title, content, authorId: userId }, include: { author: true } });
},
},
Subscription: {
postAdded: {
subscribe: () => {/* pubsub impl */},
},
},
};Resolvers type-safe avec guards auth, includes Prisma pour relations, DataLoader hooks. Ajoutez PubSub pour subs. Piège N+1 évité : batch via loaders.
Serveur Apollo avec plugins
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 './schema';
import { resolvers } from './resolvers';
import { createContext } from './context';
const app = express();
const server = new ApolloServer({
typeDefs,
resolvers,
});
await server.start();
app.use('/graphql', cors<cors.CorsRequest>(), express.json(), expressMiddleware(server, { context: createContext }));
const { url } = await startStandaloneServer(server, { listen: { port: 4000 } });
console.log(`🚀 Server ready at ${url}`);Serveur hybride Express/Standalone avec middleware context. Plugins CORS/JSON auto. Cache par défaut activé. Lancez avec ts-node src/server.ts et testez sur http://localhost:4000/graphql.
Test et subscriptions
Query exemple : { posts { title author { name } } }. Mutation : mutation { createPost(title: "Test", content: "Contenu") { id } } avec header Authorization: Bearer . Subscriptions via WS : subscription { postAdded { title } }.
Client Apollo pour tester
const { ApolloClient, InMemoryCache, gql, split, HttpLink, WebSocketLink } = require('@apollo/client');
const { getMainDefinition } = require('@apollo/client/utilities');
const httpLink = new HttpLink({ uri: 'http://localhost:4000/graphql' });
const wsLink = new WebSocketLink({ uri: 'ws://localhost:4000/graphql', options: { reconnect: true } });
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return definition.kind === 'OperationDefinition' && definition.operation === 'subscription';
},
wsLink,
httpLink
);
const client = new ApolloClient({ link: splitLink, cache: new InMemoryCache() });
client.query({ query: gql`query { posts { title } }` }).then(console.log);Client Apollo split HTTP/WS pour queries/mutations vs subs. Cache InMemory. Installez @apollo/client et exécutez pour valider. Piège : WS uri sans 'http'.
Bonnes pratiques
- Batching obligatoire : Toujours DataLoader pour relations 1:N.
- Auth par field : Guards dans resolvers, pas global.
- Caching avancé : Apollo Cache + Redis en prod.
- Rate limiting : Apollo Server plugin ou middleware Express.
- Schema stitching : Pour microservices federation.
- Monitoring : Apollo Studio ou Prometheus metrics.
Erreurs courantes à éviter
- N+1 queries : Sans DataLoader, 100 posts = 100+ DB calls.
- JWT sans verify : Token invalides crashent sans try/catch.
- Subscriptions sans PubSub :
new PubSub()from 'graphql-subscriptions'. - Prisma disconnect : Ajoutez
prisma.$disconnect()on shutdown. - Scalar manquants : Définissez DateTime avec custom scalar.
Pour aller plus loin
Approfondissez avec Apollo Federation pour microservices. Migrez vers Postgres/Kafka pour scale. Découvrez nos formations Learni sur GraphQL avancé et le workshop Prisma + Apollo.