Aller au contenu principal
    IT INNOVE - Logo de l'agence de développement web et mobile
    Retour au blog
    Architecture

    API Design: de REST à GraphQL, quand basculer?

    Mélvin Lemoine
    9 min de lecture

    Les critères qui justifient l'adoption de GraphQL et comment l'implémenter proprement.

    API Design: de REST à GraphQL, quand basculer? - Architecture | IT INNOVE
    API Design: de REST à GraphQL, quand basculer? - Architecture

    API Design: de REST à GraphQL, quand basculer?

    GraphQL brille sur les UIs riches et variées, mais impose une gouvernance de schéma. Nous présentons des cas concrets, des avantages mesurés et les pièges à éviter côté caching.

    L'évolution des besoins API en 2024

    Constats sur nos projets clients

    **Complexité croissante des interfaces
    **
    • Applications web + mobile natives + PWA + IoT
    • Besoins en données différents par plateforme
    • Over-fetching et under-fetching fréquents avec REST
    • Multiplication des endpoints pour chaque cas d'usage
    **Notre expérience comparative
    **
    • 15 APIs REST conçues et maintenues
    • 8 APIs GraphQL en production
    • 4 migrations REST → GraphQL complétées
    • 3 retours GraphQL → REST (cas spécifiques)

    Cette diversité nous donne une vision pragmatique des forces et limites de chaque approche.

    REST : les forces et limites révélées

    Quand REST excelle encore

    **Simplicité et standards
    **
    javascript
    // API REST claire et prévisible
    GET    /api/v1/users          // Liste users
    GET    /api/v1/users/123      // User spécifique
    POST   /api/v1/users          // Créer user
    PUT    /api/v1/users/123      // Modifier user
    DELETE /api/v1/users/123      // Supprimer user
    
    // Réponses standardisées
    {
      "data": { "id": 123, "name": "John Doe" },
      "meta": { "timestamp": "2024-10-22T10:00:00Z" },
      "links": {
        "self": "/api/v1/users/123",
        "posts": "/api/v1/users/123/posts"
      }
    }
    **Cache HTTP natif
    **
    javascript
    // Headers de cache optimisés
    app.get('/api/v1/users/:id', (req, res) => {
      const user = getUserById(req.params.id)
      
      res.set({
        'Cache-Control': 'public, max-age=300', // 5min
        'ETag': `"${user.updatedAt}"`,
        'Last-Modified': new Date(user.updatedAt).toUTCString()
      })
      
      // Cache conditionnel
      if (req.headers['if-none-match'] === `"${user.updatedAt}"`) {
        return res.sendStatus(304) // Not Modified
      }
      
      res.json(user)
    })

    Limites REST révélées à l'échelle

    **Over-fetching chronique
    **
    javascript
    // Mobile a besoin de: id, name, avatar
    // Desktop a besoin de: tout + permissions + stats
    // Widget a besoin de: juste name, status
    
    // REST retourne toujours tout
    GET /api/v1/users/123
    {
      "id": 123,
      "name": "John Doe",
      "email": "john@example.com",        // Pas utilisé mobile
      "address": {...},                   // Pas utilisé mobile  
      "preferences": {...},               // Pas utilisé mobile
      "permissions": [...],               // Gros objet inutile mobile
      "stats": {...}                      // Calculs coûteux non utilisés
    }
    **Multiplication endpoints
    **
    javascript
    // Explosion d'endpoints spécialisés
    GET /api/v1/users/123/mobile       // Version mobile
    GET /api/v1/users/123/widget       // Version widget
    GET /api/v1/users/123/dashboard     // Version dashboard
    GET /api/v1/users/123/profile      // Version profil
    
    // Maintenance cauchemardesque
    // Documentation dispersée  
    // Versionning complexe

    GraphQL : révolution ou complexité ?

    Les promesses tenues

    **Requêtes sur-mesure
    **
    graphql
    # Mobile: données minimales
    query MobileUser($id: ID!) {
      user(id: $id) {
        id
        name
        avatar
        isOnline
      }
    }
    
    # Dashboard: données complètes
    query DashboardUser($id: ID!) {
      user(id: $id) {
        id
        name
        email
        avatar
        stats {
          postsCount
          followersCount
          lastActive
        }
        permissions {
          canPost
          canModerate
          canAdmin
        }
        recentActivity {
          id
          type
          createdAt
        }
      }
    }
    
    # Widget: juste l'essentiel
    query WidgetUser($id: ID!) {
      user(id: $id) {
        name
        status
      }
    }
    **Relations complexes simplifiées
    **
    graphql
    # Une seule requête pour données interconnectées
    query UserWithContent($userId: ID!) {
      user(id: $userId) {
        name
        avatar
        posts(first: 10) {
          edges {
            node {
              title
              excerpt
              publishedAt
              comments(first: 3) {
                edges {
                  node {
                    content
                    author {
                      name
                      avatar
                    }
                  }
                }
              }
            }
          }
        }
        followers(first: 5) {
          edges {
            node {
              name
              avatar
              mutualFriends {
                count
              }
            }
          }
        }
      }
    }

    Architecture GraphQL moderne

    **Schéma bien structuré
    **
    graphql
    # Schema définition avec bonnes pratiques
    type User {
      id: ID!
      name: String!
      email: String
      avatar: String
      
      # Relations avec pagination
      posts(
        first: Int = 10
        after: String
        orderBy: PostOrderBy = CREATED_AT_DESC
      ): PostConnection!
      
      # Champs calculés
      fullName: String!
      isOnline: Boolean!
      
      # Métadonnées
      createdAt: DateTime!
      updatedAt: DateTime!
    }
    
    type PostConnection {
      edges: [PostEdge!]!
      pageInfo: PageInfo!
      totalCount: Int!
    }
    
    type PostEdge {
      node: Post!
      cursor: String!
    }
    
    type Post {
      id: ID!
      title: String!
      content: String!
      author: User!
      
      # Relations complexes
      comments(first: Int = 10): CommentConnection!
      tags: [Tag!]!
      
      # État
      status: PostStatus!
      publishedAt: DateTime
    }
    
    enum PostStatus {
      DRAFT
      PUBLISHED
      ARCHIVED
    }
    **Resolvers optimisés
    **
    javascript
    // Resolvers avec DataLoader pour éviter N+1
    import DataLoader from 'dataloader'
    
    const userLoader = new DataLoader(async (userIds) => {
      const users = await User.findByIds(userIds)
      return userIds.map(id => users.find(user => user.id === id))
    })
    
    const resolvers = {
      Query: {
        user: (_, { id }) => userLoader.load(id),
        
        users: async (_, { first, after }) => {
          const result = await User.paginate({ first, after })
          return {
            edges: result.users.map(user => ({ 
              node: user, 
              cursor: user.id 
            })),
            pageInfo: result.pageInfo,
            totalCount: result.totalCount
          }
        }
      },
      
      User: {
        posts: (user, { first, after }) => 
          Post.findByUser(user.id, { first, after }),
        
        followers: (user, { first }) =>
          Relationship.getFollowers(user.id, { first }),
        
        isOnline: (user) => 
          OnlineStatus.isUserOnline(user.id),
        
        fullName: (user) => 
          `${user.firstName} ${user.lastName}`
      },
      
      Post: {
        author: (post) => userLoader.load(post.authorId),
        
        comments: (post, { first }) =>
          Comment.findByPost(post.id, { first }),
        
        tags: (post) =>
          TagLoader.loadMany(post.tagIds)
      }
    }

    Performance et optimisation

    **Query complexity limiting
    **
    javascript
    import { createComplexityLimitRule } from 'graphql-query-complexity'
    
    const server = new ApolloServer({
      typeDefs,
      resolvers,
      validationRules: [
        createComplexityLimitRule(1000) // Limite complexité
      ],
      plugins: [
        {
          requestDidStart() {
            return {
              didResolveOperation(requestContext) {
                const complexity = getComplexity({
                  estimators: [
                    fieldExtensionsEstimator(),
                    simpleEstimator({ maximumComplexity: 1000 })
                  ],
                  query: requestContext.document,
                  variables: requestContext.request.variables
                })
                
                if (complexity > 1000) {
                  throw new Error(`Query too complex: ${complexity}`)
                }
              }
            }
          }
        }
      ]
    })
    **Cache intelligent multi-niveaux
    **
    javascript
    // Cache avec Redis et DataLoader
    import Redis from 'ioredis'
    const redis = new Redis(process.env.REDIS_URL)
    
    const createCachedLoader = (batchLoadFn, keyFn, ttl = 300) => {
      return new DataLoader(async (keys) => {
        // 1. Vérifier cache Redis
        const cacheKeys = keys.map(key => `loader:${keyFn(key)}`)
        const cached = await redis.mget(cacheKeys)
        
        const uncachedIndices = []
        const uncachedKeys = []
        
        cached.forEach((value, index) => {
          if (value === null) {
            uncachedIndices.push(index)
            uncachedKeys.push(keys[index])
          }
        })
        
        // 2. Charger données manquantes
        let freshData = []
        if (uncachedKeys.length > 0) {
          freshData = await batchLoadFn(uncachedKeys)
          
          // 3. Mettre en cache
          const pipeline = redis.pipeline()
          freshData.forEach((item, index) => {
            const cacheKey = `loader:${keyFn(uncachedKeys[index])}`
            pipeline.setex(cacheKey, ttl, JSON.stringify(item))
          })
          await pipeline.exec()
        }
        
        // 4. Reconstituer résultat ordonné
        const result = new Array(keys.length)
        let freshIndex = 0
        
        cached.forEach((value, index) => {
          if (value !== null) {
            result[index] = JSON.parse(value)
          } else {
            result[index] = freshData[freshIndex++]
          }
        })
        
        return result
      })
    }
    
    // Usage
    const userLoader = createCachedLoader(
      (ids) => User.findByIds(ids),
      (id) => `user:${id}`,
      600 // 10min cache
    )

    Migration progressive REST → GraphQL

    Stratégie BFF (Backend for Frontend)

    javascript
    // Gateway GraphQL devant APIs REST existantes
    const { RESTDataSource } = require('apollo-datasource-rest')
    
    class UserAPI extends RESTDataSource {
      constructor() {
        super()
        this.baseURL = 'https://api.internal.com/v1/'
      }
      
      willSendRequest(request) {
        request.headers.set('Authorization', this.context.token)
      }
      
      async getUser(id) {
        const data = await this.get(`users/${id}`)
        return this.userReducer(data)
      }
      
      async getUserPosts(userId, { first, after }) {
        const params = { limit: first, offset: after }
        const data = await this.get(`users/${userId}/posts`, params)
        return this.postsReducer(data)
      }
      
      userReducer(user) {
        return {
          id: user.id,
          name: user.full_name, // Mapping champs
          email: user.email_address,
          avatar: user.profile_image_url
        }
      }
    }
    
    // Resolvers utilisant APIs REST
    const resolvers = {
      Query: {
        user: (_, { id }, { dataSources }) =>
          dataSources.userAPI.getUser(id)
      },
      
      User: {
        posts: (user, args, { dataSources }) =>
          dataSources.userAPI.getUserPosts(user.id, args)
      }
    }

    Coexistence REST + GraphQL

    javascript
    // Router intelligent selon client
    app.use('/api', (req, res, next) => {
      const clientType = req.headers['x-client-type']
      const acceptsGraphQL = req.headers.accept?.includes('application/graphql')
      
      if (clientType === 'mobile-v2' || acceptsGraphQL) {
        return graphqlHandler(req, res, next)
      }
      
      return restHandler(req, res, next)
    })
    
    // Headers pour guider les clients
    app.use((req, res, next) => {
      res.set({
        'X-API-Version': 'v1',
        'X-GraphQL-Endpoint': '/graphql',
        'X-Rest-Endpoint': '/api/v1'
      })
      next()
    })

    Cas d'usage : quand choisir quoi ?

    GraphQL recommandé pour :

    **Applications riches avec besoins variés
    **
    javascript
    // Exemple: Dashboard admin avec widgets configurables
    query DashboardData($widgets: [WidgetType!]!) {
      user {
        name
        permissions
      }
      
      # Chargement conditionnel selon configuration
      stats @include(if: { widgets: { contains: STATS } }) {
        usersCount
        postsCount
        revenue
      }
      
      recentUsers(first: 10) @include(if: { widgets: { contains: USERS } }) {
        edges {
          node {
            name
            createdAt
            status
          }
        }
      }
      
      systemHealth @include(if: { widgets: { contains: MONITORING } }) {
        cpu
        memory
        diskSpace
      }
    }
    **Applications multi-plateformes
    **
    • Mobile : données minimales, optimisation réseau
    • Web : données complètes, relations complexes
    • IoT/widgets : données très spécifiques

    REST recommandé pour :

    **APIs publiques et partenaires
    **
    javascript
    // API claire pour partenaires externes
    GET /api/v1/products?category=electronics&limit=20
    {
      "data": [...],
      "pagination": { "next": "/api/v1/products?page=2" },
      "meta": { "total": 1234, "page": 1 }
    }
    
    // Documentation OpenAPI/Swagger native
    // Cache HTTP standard
    // Rate limiting par endpoint simple
    **CRUD simples
    **
    • Backoffice administratif
    • APIs internes microservices
    • Intégrations webhook
    • Systèmes legacy

    Performance : benchmarks réels

    Cas client : Application e-commerce

    **Avant (REST)
    **
    javascript
    // Page produit = 7 requêtes
    GET /api/products/123           // Produit
    GET /api/products/123/images    // Images  
    GET /api/products/123/reviews   // Avis
    GET /api/categories/456         // Catégorie
    GET /api/brands/789            // Marque
    GET /api/users/321             // Vendeur
    GET /api/products/related      // Produits liés
    
    // Total: 7 round-trips, 2.1s chargement mobile
    **Après (GraphQL)
    **
    graphql
    # Une seule requête optimisée
    query ProductPage($id: ID!) {
      product(id: $id) {
        name
        price
        description
        images { url, alt }
        
        category { name, slug }
        brand { name, logo }
        seller { name, rating, verified }
        
        reviews(first: 5) {
          edges {
            node {
              rating
              comment
              author { name }
              createdAt
            }
          }
          totalCount
        }
        
        relatedProducts(first: 4) {
          id
          name  
          price
          image
        }
      }
    }
    
    # Résultat: 1 requête, 0.8s chargement mobile (-62%)

    Métriques comparatives mesurées :

    | Métrique | REST | GraphQL | Amélioration |
    |----------|------|---------|--------------|
    | Requêtes/page | 5-12 | 1-2 | -75% |
    | Données transférées | 100% | 40% | -60% |
    | Time to Interactive | 2.1s | 0.8s | -62% |
    | Cache hit rate | 45% | 78% | +33% |

    Pièges GraphQL à éviter

    N+1 Problem classique

    javascript
    // ❌ Problème N+1 (1 + N requêtes)
    const resolvers = {
      User: {
        // Appelé pour chaque user dans une liste
        posts: (user) => Post.findByUserId(user.id) // ⚠️ 1 query par user
      }
    }
    
    // Query liste users = 1 + 100 requêtes si 100 users
    
    // ✅ Solution avec DataLoader
    const postsByUserLoader = new DataLoader(async (userIds) => {
      const posts = await Post.findByUserIds(userIds) // 1 seule query
      
      // Grouper par userId pour retour ordonné
      const postsByUser = userIds.map(userId => 
        posts.filter(post => post.userId === userId)
      )
      
      return postsByUser
    })
    
    const resolvers = {
      User: {
        posts: (user) => postsByUserLoader.load(user.id)
      }
    }

    Query depth bombing

    graphql
    # Attaque par requête trop profonde
    query MaliciousQuery {
      user(id: "1") {
        posts {
          author {
            posts {
              author {
                posts {
                  author {
                    posts {
                      # ... 50 niveaux de profondeur
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
    **Protection
    **
    javascript
    import depthLimit from 'graphql-depth-limit'
    
    const server = new ApolloServer({
      typeDefs,
      resolvers,
      validationRules: [
        depthLimit(10), // Max 10 niveaux
        costAnalysis({
          maximumCost: 1000,
          defaultCost: 1,
          scalars: {
            String: 1,
            Int: 1,
            Float: 2,
          },
          createError: (max, actual) => {
            return new Error(`Query cost ${actual} exceeds maximum cost ${max}`)
          }
        })
      ]
    })

    Outils et écosystème 2024

    GraphQL Stack recommandée

    **Serveur
    **
    • Apollo Server : Feature-complete, communauté
    • Yoga GraphQL : Léger, performant
    • Mercurius : Fastify, très rapide
    **Clients
    **
    • Apollo Client : Complet, cache intelligent
    • Relay : Facebook, optimisations poussées
    • URQL : Léger, moderne, flexible
    **Tooling
    **
    • GraphQL Codegen : Types TypeScript auto
    • GraphiQL : Playground intégré
    • Apollo Studio : Observabilité production

    Migration toolkit

    bash
    # Génération types TypeScript depuis schema
    npm install -D @graphql-codegen/cli @graphql-codegen/typescript
    
    # Configuration codegen.yml
    schema: "src/schema.graphql"
    generates:
      src/generated/graphql.ts:
        plugins:
          - typescript
          - typescript-resolvers
    
    # Test compatibilité breaking changes
    npm install -D @graphql-inspector/cli
    graphql-inspector diff old-schema.graphql new-schema.graphql

    Notre recommandation 2024

    GraphQL pour 60% des nouveaux projets

    **Avantages mesurés
    **
    • -60% données transférées
    • -75% requêtes réseau
    • +40% vitesse développement frontend
    • Cache plus efficace
    **Mais attention à
    **
    • Complexité opérationnelle +30%
    • Courbe d'apprentissage équipe
    • Debugging plus complexe

    REST garde sa place pour :

    • APIs publiques (documentation, standards)
    • CRUD simples et backoffices
    • Équipes junior ou legacy
    • Contraintes cache HTTP strict

    Notre processus de décision

    **Audit technique (1 semaine)
    **
    1. Analyse besoins clients (web/mobile/IoT)
    1. Évaluation équipe et expertise
    1. Cartographie données et relations
    1. POC comparatif REST vs GraphQL
    **Recommandation basée sur
    **
    • Complexité interface (simple → REST, riche → GraphQL)
    • Multi-plateforme (homogène → REST, varié → GraphQL)
    • Équipe (junior → REST, senior → GraphQL)
    • Performance requise (standard → REST, optimisée → GraphQL)

    Hésitez entre REST et GraphQL pour votre projet ? Nos architectes peuvent analyser vos besoins et vous recommander l'approche optimale. Contactez-nous pour un audit API gratuit et choisissez la bonne technologie dès le départ.

    Questions fréquentes