GraphQL est un langage de requête pour les APIs, créé par Facebook en 2012 et open-sourcé en 2015. Contrairement à REST (plusieurs endpoints), GraphQL expose un seul endpoint et c’est le client qui décrit exactement les données dont il a besoin.

GraphQL repose sur HTTP (généralement POST /graphql). Pour le protocole HTTP, voir Requêtes HTTP et HTTPS.


Le problème que GraphQL résout

Problème REST n°1 — Over-fetching :
  GET /users/42
  → { id, name, email, address, phone, preferences, avatar, ... }
  Le client voulait juste name et email → 80% des données inutiles

Problème REST n°2 — Under-fetching (N+1 requests) :
  GET /users/42         → { id, name, orders: [1, 2, 3] }
  GET /orders/1         → { ... }
  GET /orders/2         → { ... }
  GET /orders/3         → { ... }
  4 requêtes pour afficher le profil d'un utilisateur avec ses commandes

GraphQL :
  POST /graphql
  {
    user(id: "42") {
      name
      email
      orders {
        id
        total
      }
    }
  }
  → 1 seule requête, exactement les champs voulus

Schéma (SDL — Schema Definition Language)

Le schéma est le contrat entre client et serveur. Il définit les types, les relations et les opérations disponibles.

# Définition des types
type User {
  id: ID!           # ! = non-null (obligatoire)
  name: String!
  email: String!
  role: Role!
  orders: [Order!]! # liste non-null d'Order non-null
  createdAt: String
}
 
type Order {
  id: ID!
  total: Float!
  status: OrderStatus!
  items: [OrderItem!]!
}
 
enum Role {
  ADMIN
  VIEWER
  EDITOR
}
 
enum OrderStatus {
  PENDING
  SHIPPED
  DELIVERED
}
 
# Types racine
type Query {
  user(id: ID!): User
  users(role: Role, limit: Int): [User!]!
}
 
type Mutation {
  createUser(name: String!, email: String!, role: Role!): User!
  updateUser(id: ID!, role: Role): User!
  deleteUser(id: ID!): Boolean!
}
 
type Subscription {
  orderStatusChanged(orderId: ID!): Order!
}

Les 3 types d’opérations

Query — lire des données

# Requête simple
query {
  user(id: "42") {
    name
    email
  }
}
 
# Requête avec variables (bonne pratique)
query GetUser($userId: ID!) {
  user(id: $userId) {
    name
    email
    orders {
      id
      total
      status
    }
  }
}
# Variables : { "userId": "42" }
 
# Requête multiple en une seule
query Dashboard($userId: ID!) {
  user(id: $userId) { name }
  users(role: ADMIN, limit: 5) { id name }
}

Mutation — modifier des données

mutation CreateUser($input: CreateUserInput!) {
  createUser(name: $input.name, email: $input.email, role: $input.role) {
    id
    name
    createdAt
  }
}
# Variables : { "input": { "name": "Alice", "email": "alice@ex.com", "role": "ADMIN" } }

Subscription — données en temps réel

subscription TrackOrder($orderId: ID!) {
  orderStatusChanged(orderId: $orderId) {
    id
    status
    updatedAt
  }
}
# Le client reçoit une notification à chaque changement de statut
# Repose sur WebSocket ou SSE

Resolvers

Chaque champ du schéma est résolu par une fonction resolver côté serveur. Le client ne voit pas les resolvers.

// Resolver (Node.js / Apollo Server)
const resolvers = {
  Query: {
    user: (parent, args, context) => {
      // args.id = "42" (argument passé dans la requête)
      return db.users.findById(args.id);
    },
    users: (parent, args, context) => {
      return db.users.findAll({ role: args.role, limit: args.limit });
    },
  },
  User: {
    // Resolver de champ imbriqué
    orders: (user, args, context) => {
      return db.orders.findByUserId(user.id);  // N+1 potentiel → utiliser DataLoader
    },
  },
  Mutation: {
    createUser: (parent, args, context) => {
      if (!context.user.isAdmin) throw new Error("Forbidden");
      return db.users.create(args);
    },
  },
};

Fragments — réutiliser des sélections

fragment UserBasic on User {
  id
  name
  email
}
 
query {
  user(id: "1") { ...UserBasic }
  users { ...UserBasic }
}

GraphQL vs REST

CritèreRESTGraphQL
EndpointsUn par ressource (/users, /orders)Un seul (/graphql)
Over-fetchingFréquent (champs fixes)Inexistant (client choisit)
Under-fetchingFréquent (N+1 requêtes)Inexistant (requêtes imbriquées)
VersioningNécessaire (/v1, /v2)Évolution du schéma sans version
Caching HTTPNatif (GET cacheable)Difficile (tout en POST)
IntrospectionSwagger/OpenAPI externeIntégrée au protocole
Courbe d’apprentissageFaibleModérée
Idéal pourAPIs publiques simplesFrontend riche, mobile

Le problème N+1 et DataLoader

Quand un resolver fait une requête DB pour chaque élément d’une liste, on crée un problème N+1 :

Query : users { orders { ... } }

Sans DataLoader :
  SELECT * FROM users          → 3 users
  SELECT * FROM orders WHERE user_id = 1
  SELECT * FROM orders WHERE user_id = 2
  SELECT * FROM orders WHERE user_id = 3  ← N+1 requêtes

Avec DataLoader (batching) :
  SELECT * FROM users          → 3 users
  SELECT * FROM orders WHERE user_id IN (1, 2, 3)  ← 1 seule requête

Authentification et autorisation

# L'authentification se fait via les headers HTTP, comme REST
POST /graphql
Authorization: Bearer eyJ...
 
# L'autorisation se gère dans les resolvers ou avec des directives
type Query {
  adminDashboard: Stats @auth(requires: ADMIN)
}

En relation avec