GraphQL vs REST en 2025 : Pourquoi les APIs Changent
Salut HaWkers, la guerre entre GraphQL et REST APIs est loin d'être terminée, mais en 2025, le panorama est devenu beaucoup plus clair. Des entreprises comme Netflix, Shopify, GitHub et Airbnb ont migré des parties significatives de leurs APIs vers GraphQL — et il y a des raisons concrètes à cela.
Mais voici le twist : REST n'est pas mort. En fait, pour de nombreux cas d'usage, REST reste le choix le plus intelligent. Alors, comment savoir lequel utiliser ? Et plus important : comment implémenter chacun efficacement en Node.js ?
Plongeons profondément dans cette comparaison avec des exemples pratiques que vous pouvez utiliser aujourd'hui.
REST : Le Classique Qui Domine Encore
REST (Representational State Transfer) est le standard établi depuis des décennies. Sa simplicité et sa prévisibilité sont ses plus grandes forces :
// API REST traditionnelle avec Express.js
import express from 'express';
import { body, validationResult } from 'express-validator';
const app = express();
app.use(express.json());
// Base de données en mémoire (en production, utilisez une vraie DB)
let users = [
{ id: 1, name: 'Alice', email: 'alice@example.com', posts: [1, 2] },
{ id: 2, name: 'Bob', email: 'bob@example.com', posts: [3] }
];
let posts = [
{ id: 1, title: 'Intro GraphQL', authorId: 1, comments: [1] },
{ id: 2, title: 'Meilleures Pratiques REST', authorId: 1, comments: [] },
{ id: 3, title: 'Performance Node.js', authorId: 2, comments: [2, 3] }
];
let comments = [
{ id: 1, text: 'Super article !', postId: 1, authorId: 2 },
{ id: 2, text: 'Très utile', postId: 3, authorId: 1 },
{ id: 3, text: 'Merci pour le partage', postId: 3, authorId: 1 }
];
// GET /api/users - Lister tous les utilisateurs
app.get('/api/users', (req, res) => {
const { limit = 10, offset = 0 } = req.query;
const paginatedUsers = users.slice(
parseInt(offset),
parseInt(offset) + parseInt(limit)
);
res.json({
data: paginatedUsers,
pagination: {
total: users.length,
limit: parseInt(limit),
offset: parseInt(offset)
}
});
});
// GET /api/users/:id - Récupérer un utilisateur spécifique
app.get('/api/users/:id', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) {
return res.status(404).json({ error: 'Utilisateur non trouvé' });
}
res.json({ data: user });
});
// GET /api/users/:id/posts - Posts d'un utilisateur
app.get('/api/users/:id/posts', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) {
return res.status(404).json({ error: 'Utilisateur non trouvé' });
}
const userPosts = posts.filter(p =>
user.posts.includes(p.id)
);
res.json({ data: userPosts });
});
// GET /api/posts/:id/comments - Commentaires d'un post
app.get('/api/posts/:id/comments', (req, res) => {
const post = posts.find(p => p.id === parseInt(req.params.id));
if (!post) {
return res.status(404).json({ error: 'Post non trouvé' });
}
const postComments = comments.filter(c =>
post.comments.includes(c.id)
);
res.json({ data: postComments });
});
// POST /api/users - Créer un nouvel utilisateur
app.post(
'/api/users',
[
body('name').trim().isLength({ min: 2 }).escape(),
body('email').isEmail().normalizeEmail()
],
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const newUser = {
id: users.length + 1,
name: req.body.name,
email: req.body.email,
posts: []
};
users.push(newUser);
res.status(201).json({ data: newUser });
}
);
// PUT /api/users/:id - Mettre à jour un utilisateur
app.put('/api/users/:id', (req, res) => {
const userIndex = users.findIndex(u => u.id === parseInt(req.params.id));
if (userIndex === -1) {
return res.status(404).json({ error: 'Utilisateur non trouvé' });
}
users[userIndex] = {
...users[userIndex],
...req.body,
id: users[userIndex].id // Garder l'ID original
};
res.json({ data: users[userIndex] });
});
// DELETE /api/users/:id - Supprimer un utilisateur
app.delete('/api/users/:id', (req, res) => {
const userIndex = users.findIndex(u => u.id === parseInt(req.params.id));
if (userIndex === -1) {
return res.status(404).json({ error: 'Utilisateur non trouvé' });
}
users.splice(userIndex, 1);
res.status(204).send();
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`API REST en cours sur le port ${PORT}`);
});Le problème classique de REST ? Over-fetching et under-fetching. Pour obtenir un utilisateur avec ses posts et commentaires, vous devez faire plusieurs requêtes :
// Client consommant l'API REST - problème N+1
async function getUserWithPostsAndComments(userId) {
// Requête 1: Récupérer l'utilisateur
const userResponse = await fetch(`/api/users/${userId}`);
const user = await userResponse.json();
// Requête 2: Récupérer les posts de l'utilisateur
const postsResponse = await fetch(`/api/users/${userId}/posts`);
const userPosts = await postsResponse.json();
// Requêtes 3-N: Récupérer les commentaires de chaque post
const postsWithComments = await Promise.all(
userPosts.data.map(async (post) => {
const commentsResponse = await fetch(`/api/posts/${post.id}/comments`);
const comments = await commentsResponse.json();
return { ...post, comments: comments.data };
})
);
return {
...user.data,
posts: postsWithComments
};
}
// 1 + 1 + N requêtes = Problème de performance !
GraphQL : Flexibilité et Efficacité
GraphQL résout exactement ce problème. Avec une seule query, vous récupérez exactement les données dont vous avez besoin :
// API GraphQL avec Apollo Server
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
// Définir le schéma GraphQL
const typeDefs = `#graphql
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
author: User!
comments: [Comment!]!
}
type Comment {
id: ID!
text: String!
post: Post!
author: User!
}
type Query {
users(limit: Int, offset: Int): [User!]!
user(id: ID!): User
posts: [Post!]!
post(id: ID!): Post
}
type Mutation {
createUser(name: String!, email: String!): User!
updateUser(id: ID!, name: String, email: String): User!
deleteUser(id: ID!): Boolean!
createPost(title: String!, authorId: ID!): Post!
createComment(text: String!, postId: ID!, authorId: ID!): Comment!
}
`;
// Mêmes données que l'exemple REST
let users = [
{ id: 1, name: 'Alice', email: 'alice@example.com', postIds: [1, 2] },
{ id: 2, name: 'Bob', email: 'bob@example.com', postIds: [3] }
];
let posts = [
{ id: 1, title: 'Intro GraphQL', authorId: 1, commentIds: [1] },
{ id: 2, title: 'Meilleures Pratiques REST', authorId: 1, commentIds: [] },
{ id: 3, title: 'Performance Node.js', authorId: 2, commentIds: [2, 3] }
];
let comments = [
{ id: 1, text: 'Super article !', postId: 1, authorId: 2 },
{ id: 2, text: 'Très utile', postId: 3, authorId: 1 },
{ id: 3, text: 'Merci pour le partage', postId: 3, authorId: 1 }
];
// Resolvers - comment récupérer les données
const resolvers = {
Query: {
users: (_, { limit = 10, offset = 0 }) => {
return users.slice(offset, offset + limit);
},
user: (_, { id }) => {
return users.find(u => u.id === parseInt(id));
},
posts: () => posts,
post: (_, { id }) => {
return posts.find(p => p.id === parseInt(id));
}
},
User: {
posts: (parent) => {
return posts.filter(p => parent.postIds.includes(p.id));
}
},
Post: {
author: (parent) => {
return users.find(u => u.id === parent.authorId);
},
comments: (parent) => {
return comments.filter(c => parent.commentIds.includes(c.id));
}
},
Comment: {
post: (parent) => {
return posts.find(p => p.id === parent.postId);
},
author: (parent) => {
return users.find(u => u.id === parent.authorId);
}
},
Mutation: {
createUser: (_, { name, email }) => {
const newUser = {
id: users.length + 1,
name,
email,
postIds: []
};
users.push(newUser);
return newUser;
},
updateUser: (_, { id, name, email }) => {
const userIndex = users.findIndex(u => u.id === parseInt(id));
if (userIndex === -1) {
throw new Error('Utilisateur non trouvé');
}
users[userIndex] = {
...users[userIndex],
...(name && { name }),
...(email && { email })
};
return users[userIndex];
},
deleteUser: (_, { id }) => {
const userIndex = users.findIndex(u => u.id === parseInt(id));
if (userIndex === -1) return false;
users.splice(userIndex, 1);
return true;
},
createPost: (_, { title, authorId }) => {
const newPost = {
id: posts.length + 1,
title,
authorId: parseInt(authorId),
commentIds: []
};
posts.push(newPost);
// Ajouter le post à l'utilisateur
const user = users.find(u => u.id === parseInt(authorId));
if (user) {
user.postIds.push(newPost.id);
}
return newPost;
},
createComment: (_, { text, postId, authorId }) => {
const newComment = {
id: comments.length + 1,
text,
postId: parseInt(postId),
authorId: parseInt(authorId)
};
comments.push(newComment);
// Ajouter le commentaire au post
const post = posts.find(p => p.id === parseInt(postId));
if (post) {
post.commentIds.push(newComment.id);
}
return newComment;
}
}
};
// Créer le serveur Apollo
const server = new ApolloServer({
typeDefs,
resolvers
});
// Démarrer le serveur
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 }
});
console.log(`Serveur GraphQL en cours sur ${url}`);Maintenant, côté client, une seule query résout tout :
// Client GraphQL - UNE requête !
const GET_USER_WITH_DATA = `
query GetUser($userId: ID!) {
user(id: $userId) {
id
name
email
posts {
id
title
comments {
id
text
author {
name
}
}
}
}
}
`;
async function getUserWithPostsAndComments(userId) {
const response = await fetch('http://localhost:4000/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: GET_USER_WITH_DATA,
variables: { userId }
})
});
const { data } = await response.json();
return data.user;
}
// Seulement 1 requête - beaucoup plus efficace !
const user = await getUserWithPostsAndComments('1');
console.log(user);
Quand Utiliser REST vs GraphQL
Le choix n'est pas binaire. Voici un guide pratique :
Utilisez REST quand :
- API publique simple : APIs publiques avec peu d'endpoints (ex: webhooks)
- Le caching est critique : REST bénéficie du HTTP caching (CDNs, navigateurs)
- Petite équipe : Moins de complexité à maintenir
- CRUD simple : Opérations basiques sans relations complexes
- Performance prévisible : Latence et throughput constants
Utilisez GraphQL quand :
- Multiples clients : Mobile, web, desktop avec des besoins différents
- Données relationnelles : Entités avec beaucoup de relations
- Développement agile : Le schéma évolue rapidement
- L'over-fetching est un problème : La bande passante est limitée (mobile)
- Developer experience : Les grandes équipes bénéficient du typage fort
Patterns Avancés : Le Meilleur des Deux Mondes
En 2025, de nombreuses entreprises utilisent une approche hybride. Voici un exemple de comment combiner REST et GraphQL :
// Hybride: REST pour opérations simples, GraphQL pour queries complexes
import express from 'express';
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import cors from 'cors';
const app = express();
// Middleware
app.use(cors());
app.use(express.json());
// Endpoints REST pour opérations simples et publiques
app.get('/health', (req, res) => {
res.json({ status: 'healthy', timestamp: Date.now() });
});
app.post('/webhooks/stripe', express.raw({ type: 'application/json' }), (req, res) => {
// Traitement webhook (REST est meilleur ici)
const signature = req.headers['stripe-signature'];
// Traiter l'événement Stripe
res.sendStatus(200);
});
// Upload de fichier (REST est plus simple)
app.post('/api/upload', (req, res) => {
// Upload de fichier
res.json({ url: 'https://cdn.example.com/file.jpg' });
});
// GraphQL pour queries complexes et relationnelles
const apolloServer = new ApolloServer({
typeDefs,
resolvers,
// Performance: DataLoader pour batching et caching
context: async () => ({
dataSources: {
userAPI: new UserAPI(),
postAPI: new PostAPI()
}
})
});
await apolloServer.start();
// Monter l'endpoint GraphQL
app.use('/graphql', expressMiddleware(apolloServer));
// Rate limiting différencié
import rateLimit from 'express-rate-limit';
const restLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // 100 requests
});
const graphqlLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 50, // Les queries GraphQL peuvent être plus lourdes
keyGenerator: (req) => {
// Rate limit basé sur la complexité de la query
return `${req.ip}-${req.body.query ? 'complex' : 'simple'}`;
}
});
app.use('/api/*', restLimiter);
app.use('/graphql', graphqlLimiter);
app.listen(4000, () => {
console.log('Serveur API hybride en cours sur le port 4000');
console.log('Endpoints REST: /api/*');
console.log('Endpoint GraphQL: /graphql');
});DataLoader pour Éviter les Queries N+1
Un problème courant en GraphQL est le problème N+1. DataLoader le résout :
import DataLoader from 'dataloader';
// DataLoader pour batching automatique des queries
class UserAPI {
constructor() {
this.loader = new DataLoader(async (userIds) => {
console.log(`Batch loading users: ${userIds.join(', ')}`);
// Une seule query pour plusieurs IDs
const users = await db.users.findMany({
where: { id: { in: userIds } }
});
// Retourner dans le même ordre que les IDs
return userIds.map(id =>
users.find(user => user.id === id)
);
});
}
async getUser(id) {
return this.loader.load(id);
}
async getUsers(ids) {
return this.loader.loadMany(ids);
}
}
// Utiliser dans le resolver
const resolvers = {
Post: {
author: async (parent, _, { dataSources }) => {
// Plusieurs appels seront groupés automatiquement !
return dataSources.userAPI.getUser(parent.authorId);
}
},
Comment: {
author: async (parent, _, { dataSources }) => {
// Même s'il y a 100 comments, seulement 1 query à la DB
return dataSources.userAPI.getUser(parent.authorId);
}
}
};
// Avec DataLoader:
// 10 posts avec 50 comments = 1-2 queries à la DB
// Sans DataLoader:
// 10 posts avec 50 comments = 60+ queries à la DB !Le Futur des APIs en 2025 et Au-Delà
L'évolution ne s'arrête pas. Les tendances les plus excitantes incluent :
- GraphQL Federation : Microservices avec schéma unifié
- REST avec JSON:API : Standardisation des réponses REST
- tRPC : APIs type-safe sans schéma explicite
- gRPC-Web : Performance de gRPC dans le navigateur
- Server-Driven UI : APIs qui retournent des composants, pas seulement des données
Le consensus en 2025 ? Il n'y a pas de vainqueur absolu. Les meilleures architectures combinent différentes approches basées sur le cas d'usage spécifique. GraphQL brille dans les applications complexes avec plusieurs clients, tandis que REST reste imbattable pour les APIs simples et publiques.
L'important est de comprendre les trade-offs et de choisir le bon outil pour le bon problème. En tant que développeur moderne, maîtriser les deux approches vous rend beaucoup plus précieux sur le marché.
Si vous voulez approfondir l'architecture des APIs, je recommande de lire mon article sur Microservices avec Node.js : Architecture Moderne en 2025 où j'explore les patterns de communication entre services.

