Introduction Ă  GraphQL

27 juin 2019

Quand on ne connaĂźt pas GraphQL, on peut avoir l'impression qu'il s'agit d'une technologie complexe Ă  apprĂ©hender, voire un peu mystique; peut ĂȘtre Ă  cause d'un Ă©co-systĂšme et un tooling trĂšs riche (Apollo Server, Apollo Client, Prisma etc). Dans ce billet je vais essayer de montrer que GraphQL est peut ĂȘtre plus simple que vous ne l'imaginez. Les exemples seront en JavaScript car c'est mon langage actuel, mais la thĂ©orie vaut pour tous les langages : GraphQL est une spec et n'est liĂ© Ă  aucun langage en particulier.

Un language de requĂȘte simple mais puissant

GraphQL propose une maniÚre ingénieuse et intuitive d'interroger votre API, dans un format proche du JSON.

Par exemple, si je veux obtenir toutes les adresses emails des utilisateurs de mon site, je peux Ă©crire la requĂȘte suivante:

{
  users {
    email
  }
}

Dont la réponse JSON sera :

{
  "data": {
    "users": [
      {
        "email": "mail@mail.com"
      },
      {
        "email": "mail2@gmail.com"
      },
      {
        "email": "mail3@protonmail.com"
      },
      {
        "email": "mail4@yineo.fr"
      }
    ]
  }
}

Simple non ? A noter qu'on obtient uniquement les champs qu'on a demandé dans la réponse et pas les objets utilisateurs entiers, c'est une fonctionnalité de base de GraphQL.

Les arguments de champs

Nous avons la possibilité pour chaque champ d'avoir des arguments, un peu comme une fonction. Ainsi, pour paginer mes utilisateurs si j'ai beaucoup de résultats, je pourrais écrire:

{
  users(limit:20, skip: 0) {
    email
  }
}

Notez bien que chacun des champs de users peut avoir aussi des arguments. Supposons que je veuille récupérer les avatars des utilisateurs avec une taille bien spécifique pour les images (le serveur sera chargé de faire la retaille):

{
  users(limit:20, skip: 0) {
    email,
    picture(dimensions:"400x400") {
      url
    }
  }
}

Poussons le bouchon un peu plus loin avec une relation: je voudrais maintenant aussi les 5 premiers posts de blogs avec un extrait du contenu de 250 caractĂšres, pour chaque utilisateur.

{
  users(limit:20) {
    email,
    picture(dimensions:"400x400") {
      url
    }
    posts(limit:5) {
      title
      content(truncate:250)
    }
  }
}

On peut apercevoir lĂ  tout ce qu'il possible Ă  faire avec une seule requĂȘte GraphQL, avec une syntaxe qui reste trĂšs lisible mĂȘme quand les choses se corsent.

CĂŽtĂ© client : Graphql c'est juste une requĂȘte HTTP POST

Tout ce dont vous avez besoin pour envoyer une requĂȘte GraphQL Ă  un serveur GraphQL, c'est de faire une requĂȘte HTTP en POST.

Voici comment nous pouvons envoyer notre premiĂšre requĂȘte pour rĂ©cupĂ©rer les mails des users avec un simple fetch :

    fetch("http://localhost:4000/graphql", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        query: `{
          users {
            email
          }
        }`
      })
    })
    .then(response => response.json())
    .then(result => console.log("result", result));

Il existent des clients GraphQL plus ou moins complexes (Apollo Ă©tant le plus connu) mais ils sont surtout lĂ  pour ajouter des fonctionnalitĂ©s ou des helpers (pour le cache client, la gestion du token etc), ils ne sont pas indispensables en soi et ne font pas partie de GraphQL. J'ai dĂ©jĂ  rĂ©alisĂ© des projets en utilisant simplement axios pour faire mes requĂȘtes GraphQL, qui est la librairie que j'utilisais auparavant quand j'interrogerais des API REST.

CÎté serveur : créer un schema avec ses resolvers

Voici un exemple trÚs simple d'un serveur d'API GraphQL en node.js, qui permet de lister les utilisateurs d'un site. Le code est petit mais c'est bien un véritable serveur GraphQL fonctionnel. Les utilisateurs sont stockés ici dans une variable users, mais le fonctionnement serait identique avec une base de données à la place.

Installez simplement au préalable les paquets suivants :

npm install apollo-server graphql

Tuto complet : https://www.apollographql.com/docs/apollo-server/getting-started/

📝 index.js

const { ApolloServer, gql } = require('apollo-server');

const users = [
  {
    id:1,
    name: 'Yann',
    email: 'yann@mail.com',
  },
  {
    id:2,
    name: 'David',
    email: 'david@mail.com',
  },
];

// définition de notre schema GraphQL
const typeDefs = gql`
  type Query {
    user(id:ID!): User
    users: [User]
  }
  type User {
    id: ID
    name: String
    email: String
  }
`;

const resolvers = {
  Query: {
    user(parent, args) {
      return users.find(user => user.id == args.id)
    },
    users() {
      return users
    }
  }
};

const server = new ApolloServer({ typeDefs, resolvers });
server.listen().then(({ url }) => {
  console.log(`🚀  Server ready at ${url}`);
});

NOTA BENE : Pour la clarté de lecture et la concision du code dans ce billet, j'ai déclaré ci-dessus le schema en "SDL" (Schema Language Definition). Pour de gros projets, je recommanderais plutÎt d'utiliser graphql-js (https://github.com/graphql/graphql-js) pour déclarer son schema. C'est plus verbeux mais plus souple et modulaire (vous trouverez ici quelques considérations sur ce sujet : https://www.prisma.io/blog/the-problems-of-schema-first-graphql-development-x1mn4cb0tyl3)

A chaque champ son resolver

En GraphQL, on définit un schema avec des types composés de champs, tels que les types User ou Query ci-dessus. Par exemple les champs du type User sonts id, email et name.

Le type Query est spécial : tous les champs déclarés dans ce type représentent les "points d'entrées" de notre API GraphQL. Il y aussi les types spéciaux Mutation et Subscription que je n'aborderai pas dans ce billet.

Le principe de base d'un serveur GraphQL est simple : A chaque champ d'un type, on associe une fonction qui devra renvoyer sa valeur.

Prenons un exemple : supposons que le serveur GraphQL reçoive la requĂȘte suivante :

{
  users {
    email
  }
}

c'est en rĂ©alitĂ© un raccourci syntaxique pour la requĂȘte suivante( notez le "query")

query {
  users {
    email
  }
}

Le moteur d'éxécution GraphQL sur le serveur va donc d'abord chercher un champ users sur le type Query du schéma :

  type Query {
    user(id:ID!): User
    users: [User]
  }

Notre schéma déclare en effet un champ users, qui indique retourner une liste d'objets de type User.

Le moteur d'exécution GraphQL va chercher la fonction qu'il doit appeler pour "résoudre" la valeur du champ users en inspectant les resolvers

const resolvers = {
  Query: {
    user(parent, args) {
      return users.find(user => user.id == args.id)
    },
    users() {
      return users
    }
  }
};

Il y trouve bien une fonction users dĂ©finie dans les resolvers de champs du type Query. Le moteur de GraphQL exĂ©cute la fonction users() et renvoie la liste des utilisateurs comme Ă©tant la rĂ©ponse Ă  notre requĂȘte.

Vous pouvez faire TOUT CE QUE VOUS VOULEZ dans la fonction users(), la seule obligation c'est qu'elle renvoie une liste d'objets de type User, c'est Ă  dire contenant des champs id, name et email.

Pour ĂȘtre sĂ»r de bien comprendre le fonctionnement des resolvers, imaginons maintenant que nous souhaitons pouvoir recevoir les emails des utilisateurs en lettres minuscules OU majuscules, au moyen de la requĂȘte suivante:

{
  users {
    email(uppercase: true)
  }
}

On va d'abord déclarer dans notre schema l'argument uppercase sur notre champ email:

type User {
  id: ID
  name: String
  email(uppercase: Boolean): String
}

Ensuite, il nous faut déclarer un nouveau resolver pour le champ "email" de notre type "User":

const resolvers = {
  Query: {
    user(parent, args) {
      return users.find(user => user.id == args.id)
    },
    users () {
      return users
    }
  },
  User: {
    email(parent, args) {
      return args.uppercase ? parent.email.toUpperCase() : parent.email
    }
  }
};

Et le tour est joué !

Mais que signifie ce premier paramÚtre parent dans notre fonction de résolution du champ ?

Dans ce cas, le paramĂštre parent sera un "User". On aperçoit ici la nature d'arbre de GraphQL. En effet la requĂȘte pour obtenir nos email en majuscules est la suivante:

{
  users {
    email(uppercase: true)
  }
}

la valeur du champ users a déjà été "résolu" au niveau 1 par la fonction users(). Quand on arrive au niveau 2, celui de notre champ email, on peut donc accéder directement à notre user via le parent, et s'en servir pour notre fonction de résolution.

Conclusion

Il y a bien d'autres fonctionnalités intéressantes de GraphQL à explorer, mais une compréhension ce ces quelques concepts de base vous permet déjà de créer une API puissante et de profiter de certains avantages clefs de GraphQL parmi lesquels:

  • Le typage qui permet de gĂ©nĂ©rer automatiquement votre documentation dans Graphiql : au revoir les documentations pas Ă  jour ou incomplĂštes ! Le typage permet aussi de dĂ©tecter de nombreuses erreurs dans les requĂȘtes envoyĂ©es depuis le client.
  • L'explorateur Graphiql permet aussi de tester vos requĂȘtes et explorer votre API bien plus facilement et rapidement qu'avec POSTMAN ou CURL.
  • Tirer parti de la puissance des arguments pour les champs
  • AllĂ©ger certains JSON en demandant uniquement les champs dont vous avez besoin