Back-end

GraphQL + Apollo Server : API optimisée Angular

Graphql Apollo Node-Js Angular Api Apollo-Client Apollo-Server Typescript Caching Dataloader Queries Mutations Jwt Full-Stack
GraphQL + Apollo Server : API optimisée Angular

Construisez une API GraphQL scalable avec Apollo Server et Node.js, intégrez Apollo Client dans Angular : queries, mutations, caching, DataLoader et JWT.

1. REST vs GraphQL : concepts fondamentaux

REST (Representational State Transfer) est un modèle d'API basé sur des ressources et des endpoints fixés. Chaque endpoint retourne une structure prédéfinie : GET /users/123 retourne TOUTES les données utilisateur, que vous les utilisiez ou non. Cela crée deux problèmes majeurs :

  • Over-fetching : le client reçoit plus de données que nécessaire
  • Under-fetching : le client doit faire plusieurs requêtes pour obtenir toutes les données liées

GraphQL offre une alternative : le client déclare EXACTEMENT quelles données il veut, et le serveur retourne exactement ça. Une seule requête peut remplacer N requêtes REST.

Exemple :
REST: GET /users/123{ id, name, email, phone, address, createdAt, ... } (70 champs inutiles)
GraphQL: query { user(id: 123) { id, name, email } }{ id, name, email } (exactement ce qu'il faut)

Avantages GraphQL :

  • ✅ Requêtes déclaratives et optimisées
  • ✅ Une endpoint unique au lieu de N
  • ✅ Schéma auto-documenté (introspection)
  • ✅ Moins de charge réseau
  • ✅ Évolution API sans breaking changes

Quand utiliser GraphQL vs REST :

  • GraphQL : données complexes, relations variées, clients divers (web, mobile, desktop)
  • REST : CRUD simple, microservices découplés, équipes en silos

2. Installation Apollo Server

Créer un serveur Apollo GraphQL avec Express :


# Créer le projet
mkdir graphql-api && cd graphql-api
npm init -y

# Installer dépendances
npm install apollo-server express @apollo/server graphql cors dotenv
npm install --save-dev nodemon @types/node

Créer server.js :


// server.js - Serveur Apollo + Express minimal
const { ApolloServer } = require('@apollo/server');
const { startStandaloneServer } = require('@apollo/server/standalone');
const express = require('express');
const cors = require('cors');
require('dotenv').config();

// 1️⃣ DÉFINIR LE 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!
  }

  type Query {
    # Récupérer un utilisateur par ID
    user(id: ID!): User
    # Lister tous les utilisateurs
    users: [User!]!
    # Rechercher des posts
    posts(limit: Int = 10): [Post!]!
  }

  type Mutation {
    # Créer un utilisateur
    createUser(name: String!, email: String!): User!
    # Créer un post
    createPost(title: String!, content: String!, authorId: ID!): Post!
  }
`;

// 2️⃣ DONNÉES (simulées - remplacer par BD en prod)
const users = [
  { id: '1', name: 'Alice', email: 'alice@example.com' },
  { id: '2', name: 'Bob', email: 'bob@example.com' },
];

const posts = [
  { id: '1', title: 'GraphQL Guide', content: 'Article sur GraphQL...', authorId: '1' },
  { id: '2', title: 'Node.js Best Practices', content: 'Bonnes pratiques Node.js...', authorId: '2' },
];

// 3️⃣ RÉSOLVEURS (implémentation des requêtes/mutations)
const resolvers = {
  Query: {
    // Récupérer un utilisateur
    user: (parent, args) => {
      return users.find((user) => user.id === args.id);
    },

    // Lister tous les utilisateurs
    users: () => {
      return users;
    },

    // Récupérer les posts avec pagination
    posts: (parent, args) => {
      return posts.slice(0, args.limit);
    },
  },

  Mutation: {
    // Créer un utilisateur
    createUser: (parent, args) => {
      const newUser = {
        id: String(users.length + 1),
        name: args.name,
        email: args.email,
      };
      users.push(newUser);
      return newUser;
    },

    // Créer un post
    createPost: (parent, args) => {
      const newPost = {
        id: String(posts.length + 1),
        title: args.title,
        content: args.content,
        authorId: args.authorId,
      };
      posts.push(newPost);
      return newPost;
    },
  },

  // Résolveurs pour les relations (fields imbriquées)
  User: {
    posts: (parent) => {
      return posts.filter((post) => post.authorId === parent.id);
    },
  },

  Post: {
    author: (parent) => {
      return users.find((user) => user.id === parent.authorId);
    },
  },
};

// 4️⃣ CRÉER LE SERVEUR APOLLO
const server = new ApolloServer({
  typeDefs,
  resolvers,
});

// 5️⃣ DÉMARRER
async function startServer() {
  const { url } = await startStandaloneServer(server, {
    listen: { port: process.env.PORT || 4000 },
  });
  console.log(`🚀 Apollo Server lancé sur ${url}`);
}

startServer().catch(console.error);

Tester le serveur :


npm start
# Output: 🚀 Apollo Server lancé sur http://localhost:4000/

Accéder à http://localhost:4000 ouvre Apollo Sandbox (IDE GraphQL). Vous pouvez écrire des requêtes GraphQL directement dans le navigateur !

3. Schéma GraphQL et types

Le schéma est le contrat entre client et serveur. Il définit les types de données, les requêtes disponibles et les mutations.

Syntaxe de base :


# Définir un type User
type User {
  id: ID!              # ! = obligatoire (non-null)
  name: String!       # Champ obligatoire
  email: String!
  age: Int            # Optionnel (peut être null)
  posts: [Post!]!     # Liste de posts, liste obligatoire mais posts optionnels
  createdAt: DateTime!
}

# Énumération
enum Role {
  ADMIN
  USER
  GUEST
}

# Interface (contrats multiples)
interface Node {
  id: ID!
  createdAt: DateTime!
}

# Implémenter une interface
type Post implements Node {
  id: ID!
  title: String!
  createdAt: DateTime!
}

# Requête racine
type Query {
  # Requête simple
  user(id: ID!): User

  # Requête avec arguments multiples
  posts(limit: Int = 10, offset: Int = 0, status: PostStatus): [Post!]!

  # Requête avec variables (côté client)
  search(query: String!): SearchResult
}

# Type union pour résultats polymorphes
union SearchResult = User | Post | Comment

# Mutation (modification de données)
type Mutation {
  createUser(input: CreateUserInput!): User!
  updateUser(id: ID!, input: UpdateUserInput!): User
  deleteUser(id: ID!): Boolean!
}

# Input type (pour requêtes complexes)
input CreateUserInput {
  name: String!
  email: String!
  role: Role = USER
}

input UpdateUserInput {
  name: String
  email: String
}

# Subscription (temps réel)
type Subscription {
  userCreated: User!
  postUpdated(authorId: ID!): Post!
}

Meilleure pratique : Organiser les types dans des fichiers séparés


schema/
├── user.graphql      # Types User, Query.user, Mutation.createUser
├── post.graphql      # Types Post, mutations posts
├── comment.graphql   # Types Comment
└── index.js          # Combiner tous les schémas

4. Résolveurs et requêtes

Les résolveurs sont des fonctions qui retournent les données pour chaque field du schéma.


// resolvers/user.js - Résolveurs utilisateurs
const db = require('../database'); // Supposé = connexion BD

module.exports = {
  Query: {
    // Résolveur pour Query.user(id: ID!)
    user: async (parent, args, context) => {
      // parent = null pour les requêtes racines
      // args = arguments passés (ex: { id: '123' })
      // context = données partagées (user, db, etc.)

      try {
        const user = await db.User.findById(args.id);
        return user;
      } catch (error) {
        throw new Error(`Utilisateur non trouvé: ${args.id}`);
      }
    },

    // Résolveur pour Query.users
    users: async (parent, args, context) => {
      // Vérifier que l'utilisateur est authentifié (voir section auth)
      if (!context.user) {
        throw new Error('Non authentifié');
      }

      // Récupérer et filtrer
      const users = await db.User.find();
      return users;
    },

    // Résolveur avec filtrage
    posts: async (parent, args, context) => {
      let query = db.Post.find();

      // Filtrer par statut si fourni
      if (args.status) {
        query = query.where('status').equals(args.status);
      }

      // Pagination
      const posts = await query
        .limit(args.limit)
        .skip(args.offset)
        .exec();

      return posts;
    },
  },

  Mutation: {
    // Résolveur pour Mutation.createUser
    createUser: async (parent, args, context) => {
      // Validation côté serveur (IMPORTANT!)
      if (!args.input.email.includes('@')) {
        throw new Error('Email invalide');
      }

      // Vérifier que l'email n'existe pas
      const existing = await db.User.findOne({ email: args.input.email });
      if (existing) {
        throw new Error('Email déjà utilisé');
      }

      // Créer l'utilisateur
      const user = await db.User.create({
        name: args.input.name,
        email: args.input.email,
        role: args.input.role || 'USER',
      });

      return user;
    },
  },

  // Résolveurs pour les fields imbriquées
  User: {
    // Résolveur pour User.posts
    posts: async (parent, args, context) => {
      // parent = l'utilisateur en cours
      // Récupérer les posts de cet utilisateur
      return await db.Post.find({ authorId: parent.id });
    },

    // Résolveur avec DataLoader (voir section 7 pour optimisation)
    comments: async (parent, args, context) => {
      return await db.Comment.find({ userId: parent.id });
    },
  },

  Post: {
    // Résolveur pour Post.author
    author: async (parent, args, context) => {
      return await db.User.findById(parent.authorId);
    },
  },
};

Exemple requête côté client :


query {
  # Récupérer un utilisateur avec ses posts
  user(id: "123") {
    id
    name
    email
    posts {
      id
      title
    }
  }
}

Réponse serveur :


{
  "data": {
    "user": {
      "id": "123",
      "name": "Alice",
      "email": "alice@example.com",
      "posts": [
        { "id": "1", "title": "GraphQL Guide" },
        { "id": "2", "title": "Node.js Tips" }
      ]
    }
  }
}

5. Mutations et subscriptions

Mutations = modifications de données (CREATE, UPDATE, DELETE)


mutation CreatePost($title: String!, $content: String!, $authorId: ID!) {
  createPost(
    title: $title
    content: $content
    authorId: $authorId
  ) {
    id
    title
    author {
      name
    }
  }
}

Variables (côté client) :


{
  "title": "Mon Article",
  "content": "Contenu complet...",
  "authorId": "123"
}

Subscriptions = mises à jour temps réel (WebSocket)


# S'abonner aux nouveaux posts d'un auteur
subscription OnPostCreated($authorId: ID!) {
  postCreated(authorId: $authorId) {
    id
    title
    createdAt
  }
}

Implémenter côté serveur :


module.exports = {
  Subscription: {
    // S'abonner aux nouveaux posts
    postCreated: {
      subscribe: async function* (parent, args, context) {
        // Créer un pubsub pour notifications temps réel
        const asyncIterator = context.pubSub.asyncIterator(['POST_CREATED']);

        for await (const event of asyncIterator) {
          // Filter par authorId si fourni
          if (!args.authorId || event.post.authorId === args.authorId) {
            yield event;
          }
        }
      },
      resolve: (parent) => parent.post,
    },
  },

  Mutation: {
    createPost: async (parent, args, context) => {
      const newPost = await db.Post.create({
        title: args.title,
        content: args.content,
        authorId: args.authorId,
      });

      // Publier l'événement (notifier les subscribers)
      context.pubSub.publish('POST_CREATED', { post: newPost });

      return newPost;
    },
  },
};

6. Apollo Client dans Angular

Installer Apollo Client pour Angular :


ng add apollo-angular
npm install graphql apollo-angular @apollo/client graphql-tag

Configurer graphql.module.ts :


import { NgModule } from '@angular/core';
import { ApolloClientOptions, InMemoryCache, ApolloLink } from '@apollo/client/core';
import { HttpLink } from 'apollo-angular/http';
import { ApolloModule, Apollo } from 'apollo-angular';
import { setContext } from '@apollo/client/link/context';
import { HttpClientModule } from '@angular/common/http';
import { onError } from '@apollo/client/link/error';

// Options de configuration Apollo
export function createApollo(
  httpLink: HttpLink,
): ApolloClientOptions<any> {
  // Link HTTP vers serveur GraphQL
  const http = httpLink.create({
    uri: 'http://localhost:4000/graphql', // Endpoint GraphQL
  });

  // Middleware: ajouter le token JWT aux headers
  const auth = setContext((operation, { headers }) => {
    const token = localStorage.getItem('auth_token');

    // Cloner les headers et ajouter Authorization
    return {
      headers: headers ? {
        ...headers,
        Authorization: token ? `Bearer ${token}` : '',
      } : {
        Authorization: token ? `Bearer ${token}` : '',
      },
    };
  });

  // Gestion des erreurs
  const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => {
    if (graphQLErrors) {
      graphQLErrors.forEach(({ message, extensions }) => {
        console.error(`[GraphQL] ${message}`, extensions);
      });
    }
    if (networkError) {
      console.error('[Network]', networkError);
    }
  });

  // Composer les links
  const link = ApolloLink.from([errorLink, auth, http]);

  // Cache Apollo (important pour la performance)
  const cache = new InMemoryCache();

  return {
    link,
    cache,
    defaultOptions: {
      watchQuery: {
        errorPolicy: 'all', // Retourner les données même avec erreurs partielles
      },
    },
  };
}

@NgModule({
  imports: [ApolloModule, HttpClientModule],
  providers: [
    {
      provide: APOLLO_OPTIONS,
      useFactory: createApollo,
      deps: [HttpLink],
    },
  ],
})
export class GraphQLModule {}

Créer un service pour les requêtes GraphQL :


import { Injectable } from '@angular/core';
import { Apollo, gql } from 'apollo-angular';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

// Définir la requête GraphQL
const GET_USERS = gql`
  query GetUsers {
    users {
      id
      name
      email
      posts {
        id
        title
      }
    }
  }
`;

const CREATE_USER = gql`
  mutation CreateUser($name: String!, $email: String!) {
    createUser(input: { name: $name, email: $email }) {
      id
      name
      email
    }
  }
`;

@Injectable({
  providedIn: 'root'
})
export class UserService {
  constructor(private apollo: Apollo) {}

  // Récupérer tous les utilisateurs
  getUsers(): Observable<any[]> {
    return this.apollo
      .watchQuery<any>({
        query: GET_USERS,
      })
      .valueChanges.pipe(
        map((result) => result.data.users)
      );
  }

  // Créer un utilisateur
  createUser(name: string, email: string): Observable<any> {
    return this.apollo
      .mutate<any>({
        mutation: CREATE_USER,
        variables: { name, email },
      })
      .pipe(
        map((result) => result.data.createUser)
      );
  }
}

Utiliser dans un composant :


@Component({
  selector: 'app-users',
  template: `
    <div *ngIf="(users$ | async) as users; else loading">
      <div *ngFor="let user of users" class="user-card">
        {{ user.name }} ({{ user.email }})
      </div>
    </div>
    <ng-template #loading>
      Chargement...
    </ng-template>
  `
})
export class UsersComponent {
  users$: Observable<any[]>;

  constructor(private userService: UserService) {
    this.users$ = this.userService.getUsers();
  }
}

7. Performance et DataLoader

Un problème courant en GraphQL : les N+1 queries. Si vous fetchez 10 utilisateurs, alors chaque utilisateur appelle 1 requête pour ses posts = 11 requêtes totales!


query {
  users {           # 1 requête
    id
    name
    posts {         # 10 requêtes supplémentaires (N+1)!
      id
      title
    }
  }
}

Solution : DataLoader


npm install dataloader

// loaders/postLoader.js
const DataLoader = require('dataloader');
const db = require('../database');

// DataLoader batch et cache les requêtes
const createPostLoader = () => {
  return new DataLoader(async (userIds) => {
    // Requête UNIQUE pour tous les userIds au lieu de N requêtes
    const posts = await db.Post.find({
      authorId: { $in: userIds }
    });

    // Retourner dans l'ordre des userIds
    return userIds.map((userId) =>
      posts.filter((post) => post.authorId === userId)
    );
  });
};

module.exports = { createPostLoader };

Utiliser dans les résolveurs :


// resolvers/user.js
module.exports = {
  User: {
    // AVEC DataLoader (optimisé)
    posts: (parent, args, context) => {
      return context.postLoader.load(parent.id);
    },
  },
};

// server.js
const { createPostLoader } = require('./loaders/postLoader');

const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: async ({ req }) => ({
    user: req.user,
    postLoader: createPostLoader(), // 1 loader par requête
  }),
});

Caching côté client :

Apollo Client cache automatiquement les résultats. Configurer le cache :


const cache = new InMemoryCache({
  typePolicies: {
    User: {
      keyFields: ['id'], // Clé de cache unique
      fields: {
        posts: {
          merge(existing = [], incoming) {
            // Comportement de fusion du cache
            return [...existing, ...incoming];
          },
        },
      },
    },
  },
});

8. Authentification JWT en GraphQL

Passer le token JWT dans les headers Authorization et valider côté serveur :


// middleware/auth.js
const jwt = require('jsonwebtoken');

function authenticateToken(req) {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1]; // "Bearer TOKEN"

  if (!token) {
    return null; // Non authentifié (optionnel)
  }

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    return decoded; // { id, email, roles, ... }
  } catch (error) {
    throw new Error('Token invalide');
  }
}

module.exports = { authenticateToken };

Utiliser dans Apollo :


// server.js
const { authenticateToken } = require('./middleware/auth');

const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: async ({ req }) => {
    // Authentifier la requête
    const user = authenticateToken(req);

    return {
      user, // null ou objet utilisateur
      isAuthenticated: !!user,
    };
  },
});

Vérifier l'authentification dans les résolveurs :


module.exports = {
  Query: {
    // Cette requête nécessite une authentification
    me: (parent, args, context) => {
      if (!context.user) {
        throw new Error('Non authentifié');
      }
      return context.user;
    },
  },

  Mutation: {
    updateUser: (parent, args, context) => {
      if (!context.user) {
        throw new Error('Non authentifié');
      }

      // Vérifier que l'utilisateur modifie ses propres données
      if (context.user.id !== args.id && !context.user.roles.includes('ADMIN')) {
        throw new Error('Non autorisé');
      }

      return db.User.findByIdAndUpdate(args.id, args.input);
    },
  },
};

Côté Angular :


// Ajouter le token aux headers (voir section 6)
const auth = setContext((operation, { headers }) => {
  const token = localStorage.getItem('auth_token'); // Stocké après login

  return {
    headers: headers ? {
      ...headers,
      Authorization: token ? `Bearer ${token}` : '',
    } : {
      Authorization: token ? `Bearer ${token}` : '',
    },
  };
});

Conclusion

GraphQL et Apollo Server offrent une alternative moderne et puissante aux APIs REST. Grâce à l'introspection, aux requêtes déclaratives et à la gestion avancée du cache, vous pouvez construire des APIs performantes et maintenables pour des applications complexes.

📋 Checklist avant production

  • ✅ Schéma validé : types bien définis, descriptions pour chaque field
  • ✅ Authentification : JWT + vérification dans context et résolveurs
  • ✅ Validation : vérifier inputs côté serveur (jamais faire confiance au client)
  • ✅ DataLoader : éviter N+1 queries avec batch loading
  • ✅ Caching : configurer cache Apollo côté client et serveur
  • ✅ Rate limiting : limiter les requêtes par utilisateur/IP
  • ✅ Logging : logger les requêtes, mutations et erreurs
  • ✅ Tests : tester queries, mutations, sécurité (authorization)
  • ✅ Performance : profiler avec Apollo Sandbox, optimiser résolveurs lents
  • ✅ Versioning : ajouter champs sans briser les clients anciens

Questions fréquentes

GraphQL excelle quand les clients ont des besoins variés en données. REST convient mieux pour CRUD simple. GraphQL élimine l'over-fetching/under-fetching et les versions d'API.

La performance dépend de l'implémentation. GraphQL peut être plus lent en réseau (requête plus complexe) mais élimine les requêtes multiples. Optimiser avec DataLoader et caching est crucial.

Passer le token JWT dans les headers Authorization. Le contexte serveur valide et enrichit les requêtes avec les données utilisateur (middleware).

Apollo Client est plus mature et complet avec cache avancé. urql est léger et flexible. Apollo domine pour les projets complexes, urql pour les besoins simples.

Partager