Consommer une API REST avec Gatsby

⏲️ ~29 min de lecture

Publié le par Adrien
ReactJS Gatsby

Gatsby est un générateur de site statique basé sur React et GraphQL qui permet de créer des sites performants et modernes. Gatsby est couramment utilisé pour la création de site vitrine ou de blog, pour cela, il faut souvent se brancher sur une source de données distante : API REST, etc.

La documentation est complète et l'expérience développeur est très agréable, néanmoins si l'on souhaite générer des pages dynamiquement depuis une API, il reste compliqué de trouver des exemples concrets et les éléments de documentation sont dispersés.

Dans cet article, nous allons consommer une API REST avec Gatsby pour générer un petit site statique. L'API que nous allons utiliser est https://picsum.photos/ qui à l'avantage d'être gratuite et ne demande pas de clef d'API. Si vous cherchez un exemple concret, je vous invite à regarder du côté de Gatsbylius, c'est un projet open-source qui utilise Gatsby et Sylius pour générer un site e-commerce.

Pour commencer, nous allons avoir besoin de gatsby-cli. Si celui-ci n'est pas encore installé sur votre machine, vous pouvez trouver la documentation ici : Install the Gatsby CLI.

Une fois que c'est fait, on peut créer notre site Gatsby :

gatsby new gatsby-rest-consumer

On est maintenant prêt à commencer notre projet !

Fichier gatsby-node.js et tl;dr #

Le fichier gatsby-node.js nous permet d'intervenir pendant le processus de build de notre site. À l'aide des différentes API à notre disposition : Gatsby Node APIs, nous allons pouvoir créer les nodes GraphQL et générer dynamiquement les pages qui leur sont associés.

Pour les besoins de l'article, nous allons colocaliser tout le code dans ce fichier, mais il est tout à fait possible de l'extraire.

Le résultat final de l'article est le suivant (tl;dr) :

// gatsby-node.js

const fetch = require("node-fetch");
const path = require("path");
const slugify = require("slugify");

exports.sourceNodes = async ({
actions,
createNodeId,
createContentDigest
}
) => {
const { createNode } = actions;

const response = await fetch("https://picsum.photos/v2/list?limit=10");
const data = await response.json();

for (const result of data) {
const nodeId = createNodeId(`${result.id}`);
const nodeContent = JSON.stringify(result);
const node = Object.assign({}, result, {
id: nodeId,
originalId: result.id,
parent: null,
children: [],
internal: {
type: "Image",
content: nodeContent,
contentDigest: createContentDigest(result)
}
});

createNode(node);
}
};

exports.createResolvers = ({ createResolvers }) => {
const resolvers = {
Image: {
slug: {
resolve: source => {
return slugify(`${source.author} ${source.originalId}`, {
lower: true
});
}
}
}
};

createResolvers(resolvers);
};

exports.createSchemaCustomization = ({ actions }) => {
const { createTypes } = actions;

const typeDefs = `
type Image implements Node {
slug: String!
}
`
;

createTypes(typeDefs);
};

exports.createPages = async ({ graphql, actions }) => {
const { createPage } = actions;

const images = await graphql(`
query {
allImage {
nodes {
slug
}
}
}
`
);

images.data.allImage.nodes.forEach(image => {
createPage({
path: image.slug,
component: path.resolve("./src/templates/image.js"),
context: {
slug: image.slug
}
});
});
};

Nous allons construite étape par étape les différents blocks de cette configuration dans les chapitres qui suivent.

Exposer nos entités dans le schéma GraphQL #

Dans Gatsby, toutes les données doivent être accessible via GraphQL. Pour exposer des données sur le schéma GraphQL, il faut générer des Nodes. Ceux-ci sont utilisés par Gatsby afin de générer le schéma de données.

Pour rendre accessible les entités disponibles sur notre endpoint, la première étape est donc de générer les nodes qui correspondent aux images que nous récupérons via l'API https://picsum.photos/.

Nous allons utiliser sourceNodes,
cette fonction est appelée pendant l'initialisation de Gatsby et permet de créer des nouveaux nodes. Celle-ci est à exporter dans le fichier gatsby-node.js :


const fetch = require("node-fetch");

exports.sourceNodes = async ({
actions: { createNode },
createNodeId,
createContentDigest,
}
) => {
...
};

J'ai utilisé node-fetch en tant que client HTTP, pour l'installer :

npm i node-fetch

La logique est la suivante : on récupère les données depuis notre API :

const response = await fetch("https://picsum.photos/v2/list");
const data = await response.json();

puis on crée les nodes :

for (const result of data) {
const nodeId = createNodeId(`${result.id}`);

const node = Object.assign({}, result, {
id: createNodeId(`${result.id}`),
parent: null,
children: [],
internal: {
type: "Image",
content: JSON.stringify(result),
contentDigest: createContentDigest(result),
description: "Image from Picsum"
}
});

createNode(node);
}

Le format de l'objet nécessaire pour créer un node est décrit ici : createNode.

La fonction createNodeId est un petit helper qui permet de s'assurer de l'unicité de l'identifiant du node. On peut également décider d'utiliser le champ id de notre entité, mais il est privilégié de le mettre dans un autre champ si on souhaite le rendre accessible, par exemple :

const node = Object.assign({}, result, {
id: createNodeId(`${result.id}`),
originalId: result.id,
parent: null,
children: [],
internal: {
type: "Image",
content: JSON.stringify(result),
contentDigest: createContentDigest(result),
description: "Image from Picsum"
}
});

La fonction createContentDigest permet de générer un digest qui est utilisé pour invalider le cache lorsque l'on re-build.

La description n'est pas obligatoire, mais celle-ci est utilisée dans le message d'erreur en cas de conflit de type, il est donc conseillé d'en renseigner une.

Quand on rassemble les morceaux, on obtient :

// gatsby-node.js

const fetch = require("node-fetch");

exports.sourceNodes = async ({
actions: { createNode },
createNodeId,
createContentDigest
}
) => {
const response = await fetch("https://picsum.photos/v2/list");
const data = await response.json();

for (const result of data) {
const nodeId = createNodeId(`${result.id}`);
const nodeContent = JSON.stringify(result);
const node = Object.assign({}, result, {
id: nodeId,
originalId: result.id,
parent: null,
children: [],
internal: {
type: "Image",
content: nodeContent,
contentDigest: createContentDigest(result)
}
});

createNode(node);
}
};

Admirons désormais le résultat de notre travail ! Lançons Gatsby en mode dev :

npm run start

Quand le build est terminé, on peut visiter la page suivante : http://localhost:8000/___graphql pour se rendre sur GraphiQL. Celui-ci nous permet de tester nos requêtes GraphQL est de valider que nous retrouvons bien nos images. En cliquant sur le menu de gauche, on peut générer la requête rapidement.

GraphiQL

Nous allons maintenant pouvoir nous atteler à la création des pages !

Générer des pages dynamiquement #

Contrairement aux pages statiques (contenues dans le dossier pages), nous voulons créer des pages dynamiquement pour chacune de nos images, nous allons donc devoir les générer à l'aide de createPages.

Cette fonction nous permet de construire nos pages à partir des données disponibles dans GraphQL :

// gatsby-node.js

exports.createPages = async ({ graphql, actions }) => {
const { createPage } = actions;

const images = await graphql(`
query {
allImage {
nodes {
id
}
}
}
`
);

images.data.allImage.nodes.forEach(image => {
createPage({
path: image.id,
component: path.resolve("./src/templates/image.js"),
context: {
id: image.id
}
});
});
};

La première étape est de récupérer la liste des identifiants de nos images à l'aide d'une requête GraphQL, ensuite on peut utiliser l'action createPage afin de créer les pages.

Pour le moment, nous allons utiliser le champ id de nos images afin de générer les URI de nos pages (le paramètre path). On passe également l'identifiant dans l'objet context, ce qui va nous permettre de récupérer l'image via GraphQL.

Nous devonc maintenant créer le template qui est utilisé pour la génération des pages, celui-ci est très simple, c'est la partie query qui va nous interesser :

// src/templates/image.js

import React from "react";
import { graphql } from "gatsby";
import Layout from "../components/layout";

export default ({ data }) => {
const { image } = data;

return (
<Layout>
<div>
<h1>{image.author}</h1>
<img src={image.download_url} alt={image.author} />
</div>
</Layout>
);
};

export const query = graphql`
query($id: String!) {
image(id: { eq: $id }) {
download_url
author
}
}
`
;

Dans la query exportée, on peut voir l'utilisation du paramètre id que nous avons fourni via le context, celui-ci est résolu statiquement lors de la génération des pages.

Le résultat de la requête est ensuite accessible dans la propriété data de notre composant.

Après un redémarrage de Gatsby, nos pages ont été générées et sont accessibles. On peut récupérer les infos des pages disponibles sur GraphiQL en exécutant la requête suivante :

query {
allSitePage {
edges {
node {
path
}
}
}
}

Chez moi, la page http://localhost:8000/acf8db24-bd54-5d70-a9cb-15f2d96471f9 s'affiche. Les identifiants étant générés par Gatsby, cette url ne fonctionnera pas chez vous, il vous faut donc utiliser un des path qui remonte pour vous dans la requête au-dessus.

Évidemment, ces urls ne sont pas très user-friendly, nous allons améliorer cela dans le prochain chapitre.

Modifier notre schéma #

Ici, nous allons procéder à la génération d'un slug afin d'améliorer les urls de nos pages. J'ai utilisé la librairie slugify afin de faciliter leur création :

npm i slugify

Il existe deux méthodes qui permettent de rajouter un nouveau champ dans le schéma. Nous avons déjà utilisé la première solution lors de l'ajout du champ originalId, c'est à dire rajouter directement le champ lors de la création du node.

Cette solution est simple mais peut rapidement devenir lourde si on utilise plusieurs endpoints. En effet la multiplication des types va rendre compliqué l'application de cas particuliers sur la création des nodes, c'est pour cela qu'il est privilégié d'utiliser les resolvers Graphql qui vont vous permettre de simplement créer / modifier des champs sur vos types de manière déclarative.

Pour cela, nous allons utiliser la fonction createResolvers.

// gatsby-node.js

const slugify = require("slugify");

exports.createResolvers = ({ createResolvers }) => {
const resolvers = {
Image: {
slug: {
type: "String!"
resolve: source => {
return slugify(`${source.author} ${source.originalId}`, {
lower: true,
});
},
},
},
};

createResolvers(resolvers);
};

Ici nous indiquons que le type Image va désormais posséder un champ slug de type String! (le ! indique que c'est un champ requis).
La fonction resolve permet de construire la valeur que va contenir le slug. Nous utilisons le paramètre source qui contient les données initiales de l'image.

Si vous relancez Gatsby, vous devez constater l'apparition du champ slug dans vos images !

Nous pouvons maintenant mettre à jour le champ path lors de la gnération de nos pages :

// gatsby-node.js

exports.createPages = async ({ graphql, actions }) => {
const { createPage } = actions;

const images = await graphql(`
query {
allImage {
nodes {
id
slug
}
}
}
`
);

images.data.allImage.nodes.forEach(image => {
createPage({
path: image.slug,
component: path.resolve("./src/templates/image.js"),
context: {
id: image.id
}
});
});
};

On relance une nouvelle fois Gatsby :

query {
allSitePage {
edges {
node {
path
}
}
}
}

Vous devriez maintenant obtenir des urls plus ergonomiques.

La résolution des resolvers est exécutée après la génération du schéma GraphQL, notre champ n'est donc pas accessible dans les fonctions de filtre ou de tri car leur type a déjà été défini.

Ces fonctionnalités ne sont pas forcément indispensables à chaque fois, mais pour l'exemple, imaginons que nous voulons utiliser le slug à la place de l'id dans la requête de notre template. Pour résoudre ce problème, rendez-vous dans le prochain chapitre !

Utiliser des types personnalisés #

Gatsby infére automatiquement les différents types GraphQL à partir des données contenues dans les Nodes lors de la création du schéma. C'est également lors de cet étape que les types liés au filtre et au tri sont générés.

Il est possible d'ajouter manuellement des champs dans le schéma ce qui permet de corriger notre problème de filtre. En effet, l'ajout de types personnalisés impacte directement la création du schema et donc les filtres et tris sont correctement générés.

Grâce à la génération automatique de Gatsby, il n'est pas nécessaire de redéclarer tous les champs lorsque l'on en ajoute un sur un type existant, seul le nouveau champ est à déclarer.

Nous allons utiliser createSchemaCustomization.

// gatsby-node.js

exports.createSchemaCustomization = ({ actions }) => {
const { createTypes } = actions;

const typeDefs = `
type Image implements Node {
slug: String!
}
`
;

createTypes(typeDefs);
};

Nous avons ajouté un champ slug sur le type Image, à l'image de ce qui était fait dans notre resolver auparavant. Il est à noter que notre type implémente Node qui décrit les champs communs à tous les nodes (id, parent, children, ainsi que quelques champs internes comme type).

Puisque le champ est déclaré directement au niveau du schéma, il est désormais possible de retirer l'option type dans le resolver :

// gatsby-node.js

const slugify = require("slugify");

exports.createResolvers = ({ createResolvers }) => {
const resolvers = {
Image: {
slug: {
resolve: source => {
return slugify(`${source.author} ${source.originalId}`, {
lower: true
});
}
}
}
};

createResolvers(resolvers);
};

Lors de la génération des pages, on peut maintenant se passer du champ id et utiliser le slug :

// gatsby-node.js

exports.createPages = async ({ graphql, actions }) => {
const { createPage } = actions;

const images = await graphql(`
query {
allImage {
nodes {
slug
}
}
}
`
);

images.data.allImage.nodes.forEach(image => {
createPage({
path: image.slug,
component: path.resolve("./src/templates/image.js"),
context: {
slug: image.slug
}
});
});
};

Et mettre à jour la requête dans notre template :

// src/templates/image.js

export const query = graphql`
query($slug: String!) {
image(slug: { eq: $slug }) {
download_url
author
}
}
`
;

Notre champ slug est maintenant utilisable comme les autres champs GraphQL ! Cet exemple est un peu tiré par les cheveux, mais il permet de démontrer les possibilités de personnalisation du schéma GraphQL dans Gatsby.

Ce n'est pas obligatoire, mais je préfère déclarer systématiquement les nouveaux champs au niveau du createType. Cela me permet de ne pas réfléchir à la présence ou non des fonctions de filtre, etc.

Mettre en place gatsby-image #

Gatsby est connu pour sa gestion très optimisée des images, mais cela nécessite un peu de travail.

Premièrement, il faut installer la librairie gatsby-image qui sera utilisée dans nos composants pour afficher nos images. C'est ce composant qui permet d'obtenir des performances impressionnantes avec l'utilisation de version dégradées pendant le chargement, etc.

npm i gatsby-image

Il existe également gatsby-background-image pour les images en background.

Si l'on souhaite utiliser ces composants, il faut des images avec des chemins relatifs et non des urls absolues, il est donc nécessaire de télécharger les images lors du build.

Un plugin gatsby, gatsby-plugin-remote-images permet de réaliser cette opération très simplement.

npm i gatsby-plugin-remote-images

Il faut ensuite le configurer de cette manière :

// gatsby-config.js

module.exports = {
...
plugins: [
...
{
resolve: `gatsby-plugin-remote-images`,
options: {
nodeType: "Image",
imagePath: "download_url",
name: "file",
},
},
],
};

Celui-ci va automatiquement télécharger les images qui correspondent au champ download_url de nos nodes Image et les rendre disponible après transformation par les plugins gatsby-source-filesystem et gatsby-transformer-sharp qui vont générer les différentes versions optimisées des images.

Une fois le plugin configuré, on peut modifier la requête GraphQL de notre template pour qu'elle récupère les champs qui ont été générés par les différents plugins :

// src/templates/image.js

import React from "react";
import { graphql } from "gatsby";
import Img from "gatsby-image";
import Layout from "../components/layout";

export default ({ data }) => {
const { image } = data;

return (
<Layout>
<h1>{image.author}</h1>
<Img fluid={image.file.childImageSharp.fluid} alt={image.author} />
</Layout>
);
};

export const query = graphql`
query($slug: String!) {
image(slug: { eq: $slug }) {
file {
childImageSharp {
fluid(quality: 90, maxWidth: 1024) {
...GatsbyImageSharpFluid
}
}
}
author
}
}
`
;

La documentation de gatsby-image est plutôt complète, vous pouvez la retrouver ici : Gastby image. Ici, nous utilisons une image responsive(fluid) avec une largeur maximale de 1024px.

Le fragment GatsbyImageSharpFluid permet de récupérer facilement tous les champs nécessaires pour une image fluide / responsive.

Après un redémarrage de Gatsby, vous devriez constater un affichage progressif au niveau de l'image.

Petit point bonus, création d'une liste de nos images avec une taille fixe :

// pages/index.js

import React from "react";
import { Link, graphql } from "gatsby";
import Layout from "../components/layout";
import SEO from "../components/seo";
import Img from "gatsby-image";

const IndexPage = ({ data }) => {
const { allImage } = data;

return (
<Layout>
<SEO title="Home" />
<h1>Images list</h1>
<div
style=
>
{allImage.nodes.map(image => {
return (
<div style=>
<Link to={image.slug}>
<Img
fixed={image.file.childImageSharp.fixed}
alt={image.slug}
/>
</Link>
</div>
);
})}
</div>
</Layout>
);
};

export default IndexPage;

export const query = graphql`
query {
allImage {
nodes {
file {
childImageSharp {
fixed(width: 400, height: 250) {
...GatsbyImageSharpFixed
}
}
}
slug
}
}
}
`
;

Le mot de la fin #

Gatsby reste difficile à appréhender quand on le connaît mal. La documentation est certes très fournie, mais il est difficile de trouver ce que l'on veut quand on ne connait pas les termes exacts.

Cet article est la suite d'un projet dans lequel nous avons utilisé un backend Symfony et Gatsby pour générer un site statique. Passé la frustration des premiers jours, Gatsy est devenu un outil très efficace et appréciable et nous ne pouvons que le recommander .

J'espère que cet article vous facilitera la vie lors de la mise en place de vos projets !

Cet article vous a plu ? Sachez que nous recrutons !

← Accueil