Retour au blog

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 !

Appels API multiples

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 :

  1. API publique simple : APIs publiques avec peu d'endpoints (ex: webhooks)
  2. Le caching est critique : REST bénéficie du HTTP caching (CDNs, navigateurs)
  3. Petite équipe : Moins de complexité à maintenir
  4. CRUD simple : Opérations basiques sans relations complexes
  5. Performance prévisible : Latence et throughput constants

Utilisez GraphQL quand :

  1. Multiples clients : Mobile, web, desktop avec des besoins différents
  2. Données relationnelles : Entités avec beaucoup de relations
  3. Développement agile : Le schéma évolue rapidement
  4. L'over-fetching est un problème : La bande passante est limitée (mobile)
  5. 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.

C'est parti !

Commentaires (0)

Cet article n'a pas encore de commentaires. Soyez le premier!

Ajouter des commentaires