REST vs GraphQL : Guide de Choix pour votre API
Le débat entre REST et GraphQL anime la communauté depuis l'émergence de GraphQL en 2015. Ce guide vous aide à choisir la meilleure approche selon votre contexte.
Vue d'ensemble des Paradigmes
REST (Representational State Transfer)
- Architecture : Basée sur les ressources et les verbes HTTP
- Maturité : Standard établi depuis 2000
- Philosophie : "Une URL = Une ressource"
- Communication : Stateless, cacheable, interface uniforme
GraphQL
- Architecture : Basée sur un schéma et un langage de requête
- Origine : Développé par Facebook en 2012, open source en 2015
- Philosophie : "Une seule URL, plusieurs possibilités"
- Communication : Requêtes flexibles, typage fort
Comparaison Technique Détaillée
1. Structure des Requêtes
REST - Multiple Endpoints
// Récupérer un utilisateur
GET /api/users/123
{
"id": 123,
"name": "John Doe",
"email": "john@example.com"
}
// Récupérer ses posts
GET /api/users/123/posts
[
{
"id": 456,
"title": "Mon premier post",
"content": "Contenu du post..."
}
]
// Récupérer les commentaires d'un post
GET /api/posts/456/comments
GraphQL - Single Endpoint
# Une seule requête pour tout récupérer
query {
user(id: 123) {
name
email
posts {
title
content
comments {
author
text
}
}
}
}
2. Implémentation Backend
API REST avec Express.js
const express = require('express');
const app = express();
// Routes séparées pour chaque ressource
app.get('/api/users/:id', async (req, res) => {
const user = await User.findById(req.params.id);
res.json(user);
});
app.get('/api/users/:id/posts', async (req, res) => {
const posts = await Post.findByUserId(req.params.id);
res.json(posts);
});
app.post('/api/posts', async (req, res) => {
const post = await Post.create(req.body);
res.status(201).json(post);
});
app.put('/api/posts/:id', async (req, res) => {
const post = await Post.findByIdAndUpdate(req.params.id, req.body);
res.json(post);
});
app.delete('/api/posts/:id', async (req, res) => {
await Post.findByIdAndDelete(req.params.id);
res.status(204).send();
});
API GraphQL avec Apollo Server
const { ApolloServer } = require('@apollo/server');
const { startStandaloneServer } = require('@apollo/server/standalone');
// Schéma GraphQL
const typeDefs = `
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
comments: [Comment!]!
}
type Comment {
id: ID!
text: String!
author: User!
}
type Query {
user(id: ID!): User
users: [User!]!
post(id: ID!): Post
posts: [Post!]!
}
type Mutation {
createPost(title: String!, content: String!, authorId: ID!): Post!
updatePost(id: ID!, title: String, content: String): Post!
deletePost(id: ID!): Boolean!
}
`;
// Resolvers
const resolvers = {
Query: {
user: async (_, { id }) => await User.findById(id),
users: async () => await User.findAll(),
post: async (_, { id }) => await Post.findById(id),
posts: async () => await Post.findAll(),
},
Mutation: {
createPost: async (_, { title, content, authorId }) => {
return await Post.create({ title, content, authorId });
},
updatePost: async (_, { id, title, content }) => {
return await Post.findByIdAndUpdate(id, { title, content });
},
deletePost: async (_, { id }) => {
await Post.findByIdAndDelete(id);
return true;
},
},
User: {
posts: async (user) => await Post.findByUserId(user.id),
},
Post: {
author: async (post) => await User.findById(post.authorId),
comments: async (post) => await Comment.findByPostId(post.id),
},
Comment: {
author: async (comment) => await User.findById(comment.authorId),
},
};
const server = new ApolloServer({
typeDefs,
resolvers,
});
startStandaloneServer(server, { listen: { port: 4000 } });
Avantages et Inconvénients
REST
✅ Avantages
- Simplicité : Facile à comprendre et implémenter
- Cache HTTP : Support natif avec les headers HTTP
- Maturité : Écosystème riche et bien établi
- Debugging : Outils familiers (curl, Postman)
- Stateless : Scalabilité et fiabilité
// Cache HTTP avec REST
app.get('/api/users/:id', (req, res) => {
res.set('Cache-Control', 'public, max-age=300'); // 5 minutes
res.json(user);
});
// Headers utiles
res.set({
'ETag': '"123456"',
'Last-Modified': user.updatedAt,
'X-RateLimit-Remaining': '99'
});
❌ Inconvénients
- Over/Under-fetching : Récupération de données inutiles ou insuffisantes
- Multiples requêtes : Problème N+1 fréquent
- Versionning : Gestion complexe des versions
// Problème d'over-fetching
GET /api/users/123
// Retourne TOUS les champs même si on n'a besoin que du nom
// Under-fetching - plusieurs requêtes nécessaires
GET /api/users/123 // Infos utilisateur
GET /api/users/123/posts // Ses posts
GET /api/posts/456/stats // Stats du premier post
GraphQL
✅ Avantages
- Flexibilité : Récupération précise des données
- Une seule requête : Évite le problème N+1
- Typage fort : Validation automatique
- Introspection : Documentation auto-générée
- Real-time : Subscriptions intégrées
# Récupération précise des données
query GetUserDashboard {
user(id: 123) {
name
avatar
posts(limit: 5) {
title
publishedAt
viewCount
}
notifications(unreadOnly: true) {
type
message
}
}
}
# Mutations avec validation
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
id
title
slug
publishedAt
}
}
# Subscriptions temps réel
subscription {
postAdded {
id
title
author {
name
}
}
}
❌ Inconvénients
- Complexité : Courbe d'apprentissage plus raide
- Cache : Mise en cache plus complexe
- Sécurité : Risques de requêtes coûteuses
- Performance : Parsing et validation des requêtes
Patterns et Optimisations
1. REST - Optimisations
Pagination et Filtrage
// Pagination avec curseur
GET /api/posts?cursor=abc123&limit=20&sort=createdAt:desc
// Filtrage et champs spécifiques
GET /api/users?filter=active:true&fields=name,email&include=profile
// Response
{
"data": [
{ "name": "John", "email": "john@example.com", "profile": {...} }
],
"pagination": {
"nextCursor": "xyz789",
"hasMore": true,
"total": 150
}
}
HATEOAS (Hypermedia)
{
"id": 123,
"title": "Mon Post",
"status": "published",
"_links": {
"self": { "href": "/api/posts/123" },
"author": { "href": "/api/users/456" },
"comments": { "href": "/api/posts/123/comments" },
"edit": { "href": "/api/posts/123", "method": "PUT" },
"delete": { "href": "/api/posts/123", "method": "DELETE" }
}
}
2. GraphQL - Optimisations
DataLoader pour éviter N+1
const DataLoader = require('dataloader');
// Batch loading des utilisateurs
const userLoader = new DataLoader(async (userIds) => {
const users = await User.findByIds(userIds);
return userIds.map(id => users.find(user => user.id === id));
});
const resolvers = {
Post: {
author: async (post) => userLoader.load(post.authorId),
},
Comment: {
author: async (comment) => userLoader.load(comment.authorId),
},
};
Limitation de Profondeur
const depthLimit = require('graphql-depth-limit');
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [depthLimit(7)], // Max 7 niveaux de profondeur
});
Cache avec Redis
const Redis = require('ioredis');
const redis = new Redis();
const resolvers = {
Query: {
user: async (_, { id }) => {
const cached = await redis.get(`user:${id}`);
if (cached) return JSON.parse(cached);
const user = await User.findById(id);
await redis.setex(`user:${id}`, 300, JSON.stringify(user));
return user;
},
},
};
Gestion d'État Côté Client
1. REST avec React Query
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function UserProfile({ userId }) {
const queryClient = useQueryClient();
const { data: user, isLoading } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json()),
staleTime: 5 * 60 * 1000, // 5 minutes
});
const { data: posts } = useQuery({
queryKey: ['posts', userId],
queryFn: () => fetch(`/api/users/${userId}/posts`).then(r => r.json()),
enabled: !!userId,
});
const updateUser = useMutation({
mutationFn: (userData) =>
fetch(`/api/users/${userId}`, {
method: 'PUT',
body: JSON.stringify(userData),
}),
onSuccess: () => {
queryClient.invalidateQueries(['user', userId]);
},
});
if (isLoading) return <div>Chargement...</div>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
<div>
{posts?.map(post => (
<article key={post.id}>{post.title}</article>
))}
</div>
</div>
);
}
2. GraphQL avec Apollo Client
import { useQuery, useMutation, gql } from '@apollo/client';
const GET_USER_WITH_POSTS = gql`
query GetUserWithPosts($userId: ID!) {
user(id: $userId) {
name
email
posts {
id
title
content
}
}
}
`;
const UPDATE_USER = gql`
mutation UpdateUser($id: ID!, $input: UpdateUserInput!) {
updateUser(id: $id, input: $input) {
name
email
}
}
`;
function UserProfile({ userId }) {
const { data, loading } = useQuery(GET_USER_WITH_POSTS, {
variables: { userId },
pollInterval: 30000, // Refresh toutes les 30s
});
const [updateUser] = useMutation(UPDATE_USER, {
update(cache, { data: { updateUser } }) {
cache.modify({
id: cache.identify({ __typename: 'User', id: userId }),
fields: {
name: () => updateUser.name,
email: () => updateUser.email,
},
});
},
});
if (loading) return <div>Chargement...</div>;
return (
<div>
<h1>{data.user.name}</h1>
<p>{data.user.email}</p>
<div>
{data.user.posts.map(post => (
<article key={post.id}>{post.title}</article>
))}
</div>
</div>
);
}
Sécurité et Performance
1. Sécurité REST
const rateLimit = require('express-rate-limit');
const helmet = require('helmet');
// Rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requêtes par fenêtre
});
app.use(limiter);
app.use(helmet());
// Validation avec Joi
const Joi = require('joi');
const userSchema = Joi.object({
name: Joi.string().min(2).max(50).required(),
email: Joi.string().email().required(),
});
app.post('/api/users', (req, res) => {
const { error, value } = userSchema.validate(req.body);
if (error) return res.status(400).json({ error: error.details });
// Traiter la création...
});
2. Sécurité GraphQL
const { shield, rule, and, or } = require('graphql-shield');
const { createRateLimiterDirective } = require('graphql-rate-limit');
const isAuthenticated = rule({ cache: 'contextual' })(
async (parent, args, context) => {
return context.user !== null;
}
);
const isOwner = rule({ cache: 'strict' })(
async (parent, { id }, context) => {
const post = await Post.findById(id);
return post.authorId === context.user.id;
}
);
// Shield de permissions
const permissions = shield({
Query: {
user: isAuthenticated,
posts: isAuthenticated,
},
Mutation: {
createPost: isAuthenticated,
updatePost: and(isAuthenticated, isOwner),
deletePost: and(isAuthenticated, isOwner),
},
});
// Rate limiting par directive
const rateLimiterDirective = createRateLimiterDirective({
identifyContext: (context) => context.user?.id || context.ip,
});
const server = new ApolloServer({
typeDefs: `
${rateLimiterDirective.typeDefs}
type Query {
posts: [Post!]! @rateLimit(max: 100, window: "15m")
}
`,
schemaDirectives: {
rateLimit: rateLimiterDirective.directive,
},
schema: applyMiddleware(schema, permissions),
});
Guide de Choix
Choisir REST si :
- Équipe débutante en APIs
- Cache HTTP crucial pour les performances
- Architecture microservices avec services spécialisés
- APIs publiques nécessitant une large compatibilité
- Opérations CRUD simples prédominantes
Choisir GraphQL si :
- Applications mobiles avec contraintes de bande passante
- Interfaces complexes avec besoins de données variés
- Équipe expérimentée prête pour la complexité
- Développement rapide d'interfaces
- Temps réel important (subscriptions)
Conclusion
Le choix entre REST et GraphQL dépend principalement de votre contexte :
REST reste le choix de référence pour sa simplicité, sa maturité et son excellente compatibilité avec l'infrastructure web existante.
GraphQL excelle dans les contextes où la flexibilité des requêtes et l'optimisation du trafic réseau sont prioritaires.
Dans de nombreux cas, une approche hybride peut être pertinente : REST pour les APIs publiques et les opérations simples, GraphQL pour les interfaces complexes et les applications mobiles.
L'important est de choisir en fonction de vos contraintes réelles, pas des tendances du moment.
Top comments (0)