Express GraphQL

J'ai eu besoin de monter un rapide backend pour un petit site statique auquel je devais ajouter une API de blog légère. On est en 2018, j'ai donc décidé de monter cette API avec GraphQL. GraphQL, c'est un protocole proposé par facebook permettant de construire des requêtes complexes, particulièrement efficace lorsqu'il s'agit de récupérer une arborescence de données. Dans mon cas, j'aurais pu me contenter d'un rapide REST mais qu'aurais-je appris alors ? Pour la petite histoire, mon cas s'est finalement arrêté sur Firebase de Google, qui méritera lui aussi un bel article, mais ma recherche n'aura pas été vaine car elle aura au moins servi à produire cet article.

Nous allons donc construire un backend en nodejs qui servira une API GraphQL à l'aide de express-graphql. Vous pouvez déjà consulter les sources de mon exemples.

Je ne vais pas traduire la doc de express-graphql qui est vraiment très claire. Je vais simplement approfondir les sujets les plus importants (ceux pour lesquels j'ai du relire plusieurs fois la partie dédiée de la doc 😅)

Usage

Il est bon de commencer par comprendre comment on va utiliser notre API. Ça peut être pratique pour la tester, surtout si, comme moi, vous n'avez encore jamais utilisé ce type d'API.

On communique avec un serveur GraphQL par le biais de requêtes POST. Dans le body de ces requêtes, on attache un objet qui va décrire notre requête. Cet objet contient deux paramètres importants : query et variables. Le premier, c'est notre requête GraphQL, le second, c'est une liste de variables qui seront passées à la requête. Une fois qu'on a compris ça, on peut s'en foutre puisqu'on va utiliser des libs qui gèrent ça très bien toutes seules.

Ce qui est très pratique avec express-graphql, c'est qu'il inclut en option une webui permettant de se requêter lui même. Lorsque vous construisez votre serveur GraphQL, vous avez la possibilité d'activer ce serveur via le paramètre graphiql :

const graphql = graphqlHTTP({  
  schema,
  rootValue,
  graphiql: true,
});

Dans ce cas, lorsque vous taperez votre API d'un GET, vous obtiendrez une page html matérialisant une console grâce à laquelle vous allez pouvoir jouer avec votre API en cours de construction :

Pensez à la désactiver en prod.

Query

Le serveur s'attend donc à recevoir une query dans un format particulier. D'où le QL de GraphQL. Cette requête va donc décrire précisément la structure de données que vous espérez recevoir. Imaginons que nous voulons recevoir le user connecté ainsi que ses 10 derniers posts publiés. Avec une API REST, on va faire deux requêtes, la première pour avoir le user, la seconde pour avoir les posts du user. Sauf si côté backend on a pensé à faire un endpoint pour avoir ça d'un seul coup. Mais c'est pas documenté alors on sait pas. Et puis les posts, on les veut juste pour afficher le titre donc si on pouvait se passer de charger tout leur contenu, ça serait mieux. Bon. Avec GraphQL, on va juste demander ce dont on a besoin. On veut le prénom du user pour lui dire bonjour, et les titres et le slug de ses 10 derniers posts pour lui faire une liste de liens :

{
  me {
    firstName
    posts(page: 1, perPage: 10, orderBy 'publishedAt') {
      title
      slug
    }
  }
}

Et je recevrais alors un objet de ce genre :

{
  firstName: 'Georges',
  posts: [{
    title: 'Monde de merde',
    slug: 'monde-de-merde',
  }, /*x10*/]
}

Avouez que c'est assez pratique.

Côté backend, on va donc devoir définir un schéma pour pouvoir cadrer nos requêtes et renvoyer automatiquement les erreurs qui permettront au consommateur de l'API de corriger sa requête tout seul comme un grand. Ensuite, nous n'aurons plus qu'à lui attacher des resolvers, des méthodes qui vont répondre à chacune de nos définitions de requêtes.

Schema

La première étape est donc de définir un schéma d'API. On va définir des structures typées et des structures de requêtes. On pourra voir par la suite qu'on peut utiliser tous les outils qu'offrent tous langages de programmation typés telles que les interfaces par exemple, mais on va rester simple pour le moment. Notre schéma pour la requête du dessus va contenir 3 type : User, Post et Query. Les deux premiers vont décrire toutes les propriétés que peuvent avoir ces deux entités. Le dernier va décrire les méthodes qui peuvent être appelées dans une requête, leurs paramètres et le format de leurs réponses.

Ça donnera un truc dans ce genre :

{
  type Post {
    id: String!
    title: String!
    content: String!
    slug: String!
  }

  type User {
    id: String!
    firstName: String
    lastName: String
    posts: [Post]
  }

  type Query {
    me: User
    user(id: String!): User
  }
}

Donc on voit que chaque attribut de chaque type est typé. On voit aussi que certains types ont un !. Ça signifie juste que la valeur doit être non nulle. On voit aussi que certains type peuvent faire appel à d'autres. Ainsi, User possède un attribut dont la valeur est un tableau de Post. Pour finir le type Query propose deux exemples différent. Le premier est en fait une méthode sans paramètre. On va juste demander à recevoir me (donc "moi") et on recevra un objet de type User me représentant. Le second prend un paramètre nommé id et obligatoire (!) et retournera un objet de type User représentant la ressource correspondant à l'ID demandé.

Tout cela est en réalité bien plus complexe et puissant, mais le but était juste d'avoir une idée globale du concept.

Résolveurs

Oui j'adore traduire le maximum de termes anglais. On va pour finir écrire des fonctions qui seront exécutées pour récupérer les données. Ainsi, quand on demande un user et ses posts, on ira peut être chercher le user dans un PostGreSQL, et les posts dans un MongoDB : les deux types de données seront récupérées en parallèle. Avec express-graphql, c'est donc le second attribut de la fonction graphqlHTTP : rootValue. Il s'agit d'un objet listant les méthodes du type Query spécifiés dans le schema passé en premier paramètre.

Ces méthodes prendront deux paramètres : le premier est un objet contenant les variables passées à la requête. Ainsi, si je demande :

{
  user(id: 42) {
    firstName
  }
}

Alors :

{
  user({ id }) {
    return UserService.getById(id);
  }
}

id vaudra 42.

On pourra grâce au second paramètre accéder à l'objet de requête express afin d'aller récupérer des infos sur la requête et notamment la session d'authentification :

{
  me(_, { user }) {
    return UserService.getById(user.id);
  }
}

Et du coup, pour finir avec en bonus, comment gérer l'auth, même si ça sort du sujet de l'article : petit rappel pour ceux qui n'ont pas fait d'express depuis bien trop longtemps comme moi.

Auth et autres middleware

Pour gérer l'authentification, on utilise un middleware et on va ajouter des données dans l'objet req qui sera passé de middleware en middleware jusqu'à arriver à GraphQLHTTP. Ainsi, si on veut qu'un token soit passé à la requête en querystring, celui-ci permettant de décoder un ID de user, alors on configurera notre app de la sorte :

Le middleware d'authentification :

function auth(req, res, next) {  
  const { query: { token } = {} } = req;

  if (token !== '1234') { // le token est hardcodé parce que la sécurité c'est pas très important de nos jours
    return res.sendStatus(401);
  }

  req.user = UserService.getByToken(token);

  return next();
}

Qu'on placera donc avant GraphQL :

const app = express();  
app.use('/', auth, graphql);  

Et voilà, si le token "1234" n'est pas passé, alors on n'accèdera pas du tout à GraphQL. Dans le cas contraire, GraphQL aura accès à la propriété user savamment placée dans req.

Conclusion

Il est finalement possible d'architecturer à loisir votre code. Dans mon exemple, j'ai fait le choix d'avoir un seul endpoint (/) pour accéder à toutes mes données et de découper mon schema en plusieurs morceaux. On peut faire autant de endpoints qu'on veut (allez donc lire la doc d'express si vous ne savez pas comment faire), ce qui peut être notamment pratique si on veut donner à accès à certaines requêtes en public et on peut aussi n'avoir qu'un seul gros fichier pour définir le schéma complet. Et il est aussi possible d'exporter le schema définitif grâce à des outils dédiés pour servir de documentation.

Il est évidemment aussi possible de construire un serveur GraphQL avec n'importe quel autre langage qui s'exécute sur un serveur.

Hadrien

Hi, I'm a french Javascript Lead Developer, Web Architect from Toulouse, France. I've worked for 12 years for many projects with YUI, AngularJS, Aurelia.io and now React and React native.

Toulouse, France https://hadrien.eu