Depuis quelques années, la tendance est à la multiplication des micro-services pour remplacer les applications monolithiques (vous savez, ces fameuses applications qui contiennent 2000 fichiers et font crasher Eclipse au démarrage #souvenir). Côté Front, l’augmentation des endpoints nous oblige à faire de nombreux appels pour charger les données d’une seule page. Heureusement, des gentils développeurs ont pensé à créer la spécification GraphQL qui nous permet avec une seule requête de contacter de nombreux endpoints facilement et de récupérer uniquement ceux dont la page a besoin. Nous utiliserons l’implémentation JS d’Apollo Server.

Dans cet article, nous aborderons les sujets suivants : 

  • Création d’un schéma GraphQL
  • Exposition d’une query
  • Création d’une datasource REST pour envoyer des requêtes aux différents endpoints
  • Utilisation des resolvers
  • Mise en place d’un fake resolver pour mocker les appels

Présentation du projet

Le but de cet article va être de réaliser une API d’agrégation de zéro à l’aide d’Apollo Server qui est une implémentation de GraphQL. Notre API permettra de récupérer de nombreuses informations sur le Pokemon que nous aurions choisi.

 

Les informations sont les suivantes : 

  • Nom
  • Image
  • Type(s) (feu, eau, combat, psy, etc…)
  • Statistiques de base
  • Attaques acquises du niveau 1 au 100.

Le endpoint principal est : https://pokeapi.co/api/v2/pokemon/{id ou nom du pokémon}

Appelez ce endpoint avec votre client REST préféré ou faites un bon vieux curl : 

curl https://pokeapi.co/api/v2/pokemon/charmander

Dans la réponse envoyée, il y a déjà le nom (attribut name), les images (attribut sprites), les attaques (attribut moves), les statistiques (attribut stats) et les types (attribut types).

Si nous regardons l’attribut moves, nous avons juste une liste d’objet avec des id. Si nous voulons récupérer des infos supplémentaires, notamment les descriptions, il va falloir appeler un endpoint supplémentaire : https://pokeapi.co/api/v2/move/{id ou nom de l’attaque}. Nous allons avoir la même problématique pour récupérer les labels des stats et des types.

Liste des endpoints : 

Informations généraleshttps://pokeapi.co/api/v2/pokemon/{id ou nom}
Détails des attaqueshttps://pokeapi.co/api/v2/move/{id ou nom}
Détails des statshttps://pokeapi.co/api/v2/stat/{id ou nom}
Détails des typeshttps://pokeapi.co/api/v2/type/{id ou nom}

 

Il est maintenant temps de partir à la recherche des informations ! Préparez-vous à coder !

Initialisation du projet

  • Créer un nouveau dossier et se placer dans celui-ci : 
mkdir graphql-training-pokemon && cd graphql-training-pokemon
  • Initialiser le projet et télécharger les dépendances nécessaires:
npm init --yes && npm i apollo-server-core apollo-server apollo-datasource-rest graphql graphql-import && npm i -D @types/graphql
  • Créer le dossier src
  • Ajouter le fichier tsconfig.json :
{
  "compilerOptions": {
    "target": "esnext",
    "module": "commonjs",
    "outDir": "dist",
    "strict": true,
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": false,
    "resolveJsonModule": true,
    "sourceMap": true,
    "baseUrl": ".",
    "paths": {
      "@src/*": ["./src/*"]
    }
  },
  "exclude": [
    "node_modules"
  ]
}
  • Ajouter les scripts « build » et « start » dans package.json
"scripts": {
  "build": "tsc && cp src/schema.graphql dist",
  "start": "npm run build && node dist/index.js"
}
  • Ajouter dans le dossier src le fichier schema.graphql : 
type Pokemon {
  img: String!
  name: String!
  types: [String]!
  moves: [Move]!
  stats: [Stat]!
}

type Move {
  id: ID!
  name: String!
  description: String
  accuracy: Int
  damageClass: String
  power: Int
  pp: Int
}

type Stat {
  name: String!
  base: Int!
}

type Query {
  getPokemonByName(name: String!): Pokemon
}

Le typage est relativement simple, vous avez les types primitifs suivants : Int, Float, String, Boolean, ID. Les ! permettent de préciser que le champ n’est pas nullable.

Dans la majorité du fichier GraphQL, vous retrouvez des objets. Cependant, il y a 2 types spéciaux : Query et Mutation. Ces objets sont spéciaux car ils définissent le point d’entrée de votre API. Par exemple, dans notre schéma, nous exposons une query getPokemonByName.

Pour en savoir plus sur le typage, je vous conseille de vous rendre sur la page dédiée présente dans la documentation de GraphQL.

  • Créer le fichier index.ts dans le dossier src pour pouvoir démarrer le serveur en important le schéma graphql :
import { ApolloServer, gql } from 'apollo-server';
import { importSchema } from 'graphql-import';
import { join } from 'path';

const typeDefs = gql(importSchema(join(__dirname, 'schema.graphql')));

const server = new ApolloServer({ typeDefs });

server.listen().then(({ url }): any => {
  console.log(`Server ready at ${url}`);
});
  • Ajouter dans le dossier src les dossiers : data, datasources, models, resolvers
  • Lancer la commande suivante dans votre terminal et vérifier que le serveur démarre : 
npm start

Comme nous travaillons en TypeScript, nous allons créer de nombreux modèles de données, dans le fichier src/models/index.ts, placez le contenu suivant : 

export interface PokemonApi {
  id: string;
  name: string;
  types: [TypeApi];
  moves: [RedirectInfoMoveApi];
  stats: [RedirectInfoStatApi];
  sprites: SpritesApi;
}

export interface RedirectInfoMoveApi {
  move: {
    name: string;
    url: string;
  }
}

export interface RedirectInfoStatApi {
  base_stat: number;
  effort: number;
  stat: {
    name: string;
    url: string;
  }
}

export interface TypeApi {
  slot: number;
  type: {
    name: string;
    url: string;
  }
}

export interface SpritesApi {
  back_default: string;
  front_default: string;
}

export interface MoveApi {
  accuracy: number;
  id: number;
  names: NameApi[];
  power: number;
  pp: number;
  flavor_text_entries: FlavorTextApi[],
  damage_class: {
    name: string;
  }
}

export interface StatApi {
  names: NameApi[]
}

export interface NameApi {
  name: string;
  language: {
    name: string;
  }
}

export interface FlavorTextApi {
  flavor_text: string;
  language: {
    name: string;
  }
}

export interface GetPokemonByNameArgument {
  name: string;
}

export interface Pokemon {
  img: string;
  name: string;
  types: string[];
  moves: string[];
  stats: Stat[];
}

export interface Move {
  id: number;
  name: string;
  description: string;
  accuracy: number;
  damageClass: string;
  power: number;
  pp: number;
}

export interface Stat {
  name: string;
  base: number;
}

Les interfaces finissant par Api sont les modèles de données que l’API va nous retourner.

L’interface GetPokemonByNameArgument représente les arguments que nous allons envoyer depuis la query GraphQL.

Les autres interfaces représenteront les objets que nous manipulerons après retour de l’API. 

Récupération des infos basiques à l’aide d’un seul endpoint

Nous allons commencer par récupérer les infos basiques du pokémon (sprite, nom et types). Toutes ces informations sont récupérables directement à l’aide du endpoint : https://pokeapi.co/api/v2/pokemon/{id ou nom}

Dans le dossier datasources, on ajoute le fichier PokemonRestDataSource.ts

import { RESTDataSource } from 'apollo-datasource-rest';

import { PokemonApi, GetPokemonByNameArgument, Pokemon } from '../models';

export class PokemonRestDataSource extends RESTDataSource {
  constructor() {
    super();
    this.baseURL = 'https://pokeapi.co/api/v2/';
  }

  getPokemonByName(args: GetPokemonByNameArgument) {
    return this.get<PokemonApi>(`pokemon/${args.name}`)
      .then(this.pokemonReducer);
  }

  pokemonReducer = (response: PokemonApi): Pokemon => (
    {
      img: response.sprites.front_default,
      name: response.name,
      types: response.types.map(typeApi => typeApi.type.name),
      moves: response.moves.map(moveDetail => moveDetail.move.name),
      stats: response.stats.map(statDetail => ({ name: statDetail.stat.name, base: statDetail.base_stat }))
    }
  );
}

Explication : La classe étend RESTDataSource car nous allons contacter une API REST. Nous pouvons alors renseigner l’attribut baseURL qui sera la base de l’URL pour tous nos appels. Dans la méthode getPokemonByName, nous exécutons un appel en GET grâce à la méthode get de la classe héritée. On précise ici que nous allons récupérer un objet de type PokemonApi et nous appelons le endpoint pokemon/{args}. Une fois que l’appel a été effectué et la réponse récupérée, nous pouvons, à l’aide d’un reducer, mapper les informations dont nous avons besoin.

Pour utiliser cette datasource, on la lie avec le serveur Apollo, dans src/index.ts

const server = new ApolloServer({
  typeDefs,
  resolvers,
  dataSources: () => ({
    pokemonRestDataSource: new PokemonRestDataSource(),
  })
});

Il faut maintenant écrire le resolver qui permet de faire le lien entre le schéma graphQL et le code du serveur. Créez le fichier pokemon-resolver.ts dans le dossier resolvers

import { GetPokemonByNameArgument } from '../models';

export function getPokemonByName(_: any, args: GetPokemonByNameArgument, { dataSources }: any) {
  return dataSources.pokemonRestDataSource.getPokemonByName({ name: args.name });
}

Un resolver contient 4 arguments : 

  • parent: Un objet qui contient les résultats retournés par un resolver sur le parent. Nous y reviendrons dans la suite du tutoriel
  • args: Les arguments passés au champ
  • context: Un objet qui est partagé entre tous les resolvers
  • info: Information concernant l’état d’exécution de l’opération

Pour éviter d’avoir { dataSources }: any, déclarez une interface GlobalContext qui va définir les datasources.

Ajoutez un fichier src/apollo-context.ts

import { PokemonRestDataSource } from './datasources/PokemonRestDataSource';

export interface GlobalContext {
  dataSources: {
    ['pokemonRestDataSource']: PokemonRestDataSource,
  }
}

On peut désormais mettre à jour la classe PokemonRestDataSource :

export class PokemonRestDataSource extends RESTDataSource<GlobalContext> {...}


Ainsi que le resolver pokemon-resolver

import { GetPokemonByNameArgument } from '../models';
import { GlobalContext } from '../apollo-context';

export function getPokemonByName(_: any, args: GetPokemonByNameArgument, { dataSources }: GlobalContext) {
  return dataSources.pokemonRestDataSource.getPokemonByName({ name: args.name });
}

Créez un fichier src/resolvers/index.ts qui listera l’ensemble de nos resolvers. Comme nous travaillons uniquement sur des requêtes de type Query, le resolver contiendra un objet Query. 

import { IResolvers } from 'graphql-tools';

import { GlobalContext } from '../apollo-context';
import { getPokemonByName } from './pokemon-resolver';

export const resolvers: IResolvers<any, GlobalContext> = {
  Query: {
    getPokemonByName
  }
};

Le resolver expose une seule requête getPokemonByName. D’autres requêtes vont venir s’ajouter dans les prochaines étapes.

Il va falloir mettre à jour le serveur afin qu’il puisse utiliser notre resolver. Importez le resolver dans src/index.ts et ajoutez-le dans les arguments du serveur Apollo :

import { ApolloServer, gql } from 'apollo-server';
import { importSchema } from 'graphql-import';
import { join } from 'path';
import { PokemonRestDataSource } from './datasources/PokemonRestDataSource';
import { resolvers } from './resolvers';

const typeDefs = gql(importSchema(join(__dirname, 'schema.graphql')));

const server = new ApolloServer({
  typeDefs,
  resolvers,
  dataSources: () => ({
    pokemonRestDataSource: new PokemonRestDataSource(),
  })
});

server.listen().then(({ url }): any => {
  console.log(`Server ready at ${url}`);
});

Démarrez le serveur en utilisant npm start et envoyez la requête suivante en vous rendant sur http://localhost:4000 :

query {
  getPokemonByName(name: "butterfree") {
    img
    name
    types
  }
}

Vous devriez obtenir la réponse suivante : 

{
  "data": {
    "getPokemonByName": {
      "img": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/12.png",
      "name": "butterfree",
      "types": [
        "flying",
        "bug"
      ]
    }
  }
}

Bravo ! Nous allons maintenant écrire d’autres resolvers pour récupérer les différentes attaques et les statistiques de base du pokemon !

Création d’un resolver pour récupérer les mouvements

Vous vous souvenez que nous avons récupéré les noms des mouvements dans les informations du pokémon. Nous allons pouvoir nous en servir dès à présent. Dans le fichier PokemonRestDataSource.ts, ajoutez les fonctions suivantes : 

import { RESTDataSource } from 'apollo-datasource-rest';
import { GlobalContext } from '../apollo-context';

import { PokemonApi, GetPokemonByNameArgument, Pokemon, Move, MoveApi, NameApi, FlavorTextApi } from '../models';

export class PokemonRestDataSource extends RESTDataSource<GlobalContext> {
  ...

  getMovesByNames(names: string[]) {
    return Promise.all(names.map(name => this.get<MoveApi>(`move/${name}`)))
      .then(results => results.map(this.moveReducer));
  }

  moveReducer = (response: MoveApi): Move => (
    {
      id: response.id,
      name: this.moveNameReducer(response.names),
      accuracy: response.accuracy,
      damageClass: response.damage_class.name,
      power: response.power,
      pp: response.pp,
      description: this.moveDescriptionReducer(response.flavor_text_entries)
    }
  );

  moveDescriptionReducer = (textEntries: FlavorTextApi[]): string => {
    const flavor = textEntries.find(entry => entry.language.name === 'en');
    return flavor ? flavor.flavor_text : 'Description not found';
  }

  moveNameReducer = (names: NameApi[]): string => {
    const description = names.find(moveName => moveName.language.name === 'en');
    return description ? description.name : 'Move not found';
  }
}

Nous nous basons sur la liste des noms pour récupérer tous les mouvements associés. Mais comment faire pour appeler cette méthode et lui passer la liste des noms ? Grâce aux resolvers, bien sûr !

Créez un nouveau resolver move-resolver.ts dans le dossier resolvers.

import { Pokemon } from '../models';
import { GlobalContext } from '../apollo-context';

export function getMovesByNames(pokemon: Pokemon, _: any, { dataSources }: GlobalContext) {
  return dataSources.pokemonRestDataSource.getMovesByNames(pokemon.moves);
}

Explication : Si vous avez fait attention; nous ne récupérons plus les informations du 2ème argument mais bien du 1er qui est le parent, mais comment le parent est arrivé là ?

Mettez à jour le fichier resolvers/index.ts :

import { IResolvers } from 'graphql-tools';

import { GlobalContext } from '../apollo-context';
import { getPokemonByName } from './pokemon-resolver';
import { getMovesByNames } from './move-resolver';

export const resolvers: IResolvers<any, GlobalContext> = {
  Query: {
    getPokemonByName
  },
  Pokemon: {
    moves: getMovesByNames
  }
};

Explication : Voici comment le parent arrive dans la méthode getMovesByNames. Dès que vous allez mettre moves dans la requête GraphQL pour le schéma Pokemon, le resolver va automatiquement appeler la méthode voulue en passant le parent.

On lance la requête : 

query {
  getPokemonByName(name: "butterfree") {
    img
    name
    types
    moves {id name description damageClass pp power accuracy}
  }
}

Vous devriez maintenant récupérer la liste des mouvements du pokémon !

Maintenant que nous avons compris le fonctionnement des resolvers, nous allons pouvoir faire la même chose pour les statistiques !

Création d’un autre resolver pour récupérer les statistiques


Dans la requête /pokemon/{name}, nous récupérons déjà la valeur des stats, mais nous allons récupérer les labels traduits. Pour cela, ajoutez une méthode dans PokemonRestDataSource qui va à partir d’un objet Stat récupérer le label traduit en anglais :

import { RESTDataSource } from 'apollo-datasource-rest';
import { GlobalContext } from '../apollo-context';

import { PokemonApi, GetPokemonByNameArgument, Pokemon, Move, MoveApi, NameApi, FlavorTextApi, StatApi, Stat } from '../models';

export class PokemonRestDataSource extends RESTDataSource<GlobalContext> {
  ...

  getLocalizedStatName(stat: Stat) {
    return this.get<StatApi>(`stat/${stat.name}`).then(this.statReducer);
  }

  ...

  statReducer = (response: StatApi): string => {
    const statName = response.names.find(entry => entry.language.name === 'en');
    return statName ? statName.name : 'Stat not found';
  }
}

Créez un nouveau resolver stat-resolver.ts qui aura une fonction getLocalizedStatName avec comme paramètre le parent Stat et appellera la datasource. 

import { Stat } from '../models';
import { GlobalContext } from '../apollo-context';

export function getLocalizedStatName(stat: Stat, _:any, { dataSources }: GlobalContext) {
  return dataSources.pokemonRestDataSource.getLocalizedStatName(stat);
}

Il ne reste plus qu’à mettre à jour le fichier resolvers/index.ts

export const resolvers: IResolvers<any, GlobalContext> = {
  Query: {
    getPokemonByName
  },
  Pokemon: {
    moves: getMovesByNames
  },
  Stat: {
    name: getLocalizedStatName
  }
};

On lance la requête :

query {
  getPokemonByName(name: "butterfree") {
    img
    name
    types
    moves {id name description damageClass pp power accuracy}
    stats {name base}
  }
}

Vous devriez recevoir maintenant la liste des types avec des labels traduits.

Passer des données dans le header

Pour le moment, nous récupérons toujours des labels traduits en anglais. Il serait intéressant de passer un paramètre language dans le header pour traduire les labels dans la langue souhaitée.

Pour cela, nous allons ajouter dans le contexte l’attribut language

apollo-context.ts : 

import { PokemonRestDataSource } from './datasources/PokemonRestDataSource';

export interface GlobalContext {
  dataSources: {
    ['pokemonRestDataSource']: PokemonRestDataSource,
  };
  language: string;
}

Ajoutons dans les paramètres du serveur Apollo l’attribut context qui est une fonction qui mettra à jour le contexte en fonction de la requête : 

import { IncomingMessage, ServerResponse } from 'http';
...

interface RequestResponseWrapper {
  req: IncomingMessage;
  res: ServerResponse;
}

const server = new ApolloServer({
  typeDefs,
  resolvers,
  dataSources: () => ({
    pokemonRestDataSource: new PokemonRestDataSource(),
  }),
  context: (arg: RequestResponseWrapper) => ({
    language: arg.req.headers.language as string
  })
});

Nous allons maintenant pouvoir utiliser la langue stockée dans le contexte et récupérer les labels avec la langue que nous souhaitons.

Mettons à jour la partie datasource PokemonRestDataSource, on accède au contexte juste en faisant this.context.

moveDescriptionReducer = (textEntries: FlavorTextApi[]): string => {
    const flavor = textEntries.find(entry => entry.language.name === this.context.language);
    return flavor ? flavor.flavor_text : 'Description not found';
  }

moveNameReducer = (names: NameApi[]): string => {
  const description = names.find(moveName => moveName.language.name === this.context.language);
  return description ? description.name : 'Move not found';
}

statReducer = (response: StatApi): string => {
  const statName = response.names.find(entry => entry.language.name === this.context.language);
  return statName ? statName.name : 'Stat not found';
}

Retournez sur http://localhost:4000 et ajoutez le header suivant : 

{
  "language": "fr"
}

Vous devriez avoir les labels des mouvements et des stats en français.

Retourner une erreur

Il est possible de retourner des erreurs personnalisées. Pour l’exemple, nous retournerons une erreur Apollo quand le pokemon est de type « bug ».

Nous rechercherons dans la liste des types retournés par l’API, si la liste contient la chaîne de caractères « bug » alors nous enverrons une ApolloError avec le message ‘OMG there is a bug’ et un code 500.

PokemonRestDataSource.ts : 

getPokemonByName(args: GetPokemonByNameArgument) {
  return this.get<PokemonApi>(`pokemon/${args.name}`)
    .then(pokemon => {
      if (pokemon.types.find(typeApi => typeApi.type.name === 'bug')) {
        throw new ApolloError('OMG there is a bug !', '500');
      }
      return this.pokemonReducer(pokemon);
    });
}

Relancez le serveur et rejouez la query avec comme paramètre « butterfree », vous allez obtenir : 

{
  "errors": [
    {
      "message": "OMG there is a bug !",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "getPokemonByName"
      ],
      "extensions": {
        "code": "500",
        "exception": {
          "stacktrace": [
            "Error: OMG there is a bug !",
            "    at get.then.result (/graphql-pokemon-training/dist/datasources/PokemonRestDataSource.js:42:23)",
            "    at processTicksAndRejections (internal/process/next_tick.js:81:5)"
          ]
        }
      }
    }
  ],
  "data": {
    "getPokemonByName": null
  }
}

Essayez maintenant avec un pokemon n’ayant pas le type « bug », par exemple « charmander », vous devriez récupérer les informations.

Créer un fake resolver pour mocker vos appels

Dans un projet, il n’est pas rare que certains endpoints ne soient pas encore prêts (sauf dans le monde des Bisounours). Pour ne pas rester bloqué, il est possible de créer un resolver qui utilisera des fausses données. Un fake resolver peut aussi être utile lorsque vous faites des tests fonctionnels.

Dans le dossier resolvers, ajoutez le dossier fake

Dans le dossier data, ajoutez pokemon.json et moves.json.

pokemon.json :

[
  {
    "img": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/4.png",
    "name": "charmander",
    "types": [
      "fire"
    ],
    "moves": ["ultimapoing", "claw"],
    "stats": [{
      "base": 66,
      "name": "hp"
    }, {
      "base": 90,
      "name": "attack"
    }]
  },
  {
    "img": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/1.png",
    "name": "bulbasaur",
    "types": [
      "grass"
    ],
    "moves": ["tackle", "sleep-powder"],
    "stats": [{
      "base": 100,
      "name": "hp"
    }, {
      "base": 75,
      "name": "attack"
    }]
  }
]

moves.json

[
  {
    "id": 5,
    "name": "ultimapoing",
    "description": "L’ennemi reçoit un coup de poing d’une puissance\nincroyable.",
    "damageClass": "physical",
    "pp": 20,
    "power": 80,
    "accuracy": 85
  },
  {
    "id": 10,
    "name": "claw",
    "description": "Lacère l’ennemi avec des griffes acérées pour lui\ninfliger des dégâts.",
    "damageClass": "physical",
    "pp": 35,
    "power": 40,
    "accuracy": 100
  },
  {
    "id": 33,
    "name": "tackle",
    "description": "Le lanceur charge l’ennemi et le percute de tout\nson poids.",
    "damageClass": "physical",
    "pp": 35,
    "power": 40,
    "accuracy": 100
  },
  {
    "id": 79,
    "name": "sleep-powder",
    "description": "Le lanceur répand une poudre soporifique qui\nendort la cible.",
    "damageClass": "status",
    "pp": 15,
    "power": 0,
    "accuracy": 75
  }
]

Pour réaliser le fake resolver, la structure est exactement la même que le resolver que nous avons fait, la différence est qu’il faudra importer les fichiers de données et récupérer les données depuis les fichiers.

resolvers/fake/index.ts : 

import { IResolvers } from 'graphql-tools';

import { GlobalContext } from '../../apollo-context';
import { Pokemon, GetPokemonByNameArgument, Move, Stat } from '@src/models';

import * as filePokemon from '../../data/pokemon.json';
import * as fileMove from '../../data/moves.json';
const fakePokemons: Pokemon[] = [...filePokemon];
const fakeMoves: Move[] = [...fileMove];

export const fakeResolvers: IResolvers<any, GlobalContext> = {
  Query: {
    getPokemonByName(_: any, args: GetPokemonByNameArgument, __: any) {
      return Promise.resolve(fakePokemons.find(p => p.name === args.name));
    }
  },
  Pokemon: {
    moves(parent: Pokemon) {
      const foundMoves = parent.moves.map(
        move => fakeMoves.find(fakeMove => fakeMove.name === move)
      );
      return Promise.resolve(foundMoves);
    }
  },
  Stat: {
    name(parent: Stat) {
      return Promise.resolve(parent.name);
    }
  }
};

Mettez à jour le fichier src/index.ts pour pouvoir choisir entre le fake et le vrai resolver : 

const server = new ApolloServer({
  typeDefs,
  resolvers: 'tests' === process.env.NODE_ENV ? fakeResolvers : resolvers,
  dataSources: () => ({
    pokemonRestDataSource: new PokemonRestDataSource(),
  }),
  context: (arg: RequestResponseWrapper) => ({
    language: arg.req.headers.language as string
  })
});

Démarrez le serveur avec la commande : 

NODE_ENV=tests npm start

Envoyez la query suivante : 

query {
  getPokemonByName(name: "bulbasaur") {
    img
    name
    types
    moves {id name description damageClass pp power accuracy}
    stats {base, name}
  }
}

Vous avez bien récupéré les données du fake !

Redémarrez le serveur avec la commande : 

npm start

Rejouez la query, vous avez récupéré les vrais données !

Conclusion

GraphQL est une bonne spécification pour agréger des données. Il est relativement simple de mettre des données en relation avec différents endpoints avec l’implémentation proposée par Apollo Server. Avec la multiplication des micro-services, GraphQL pourrait bien devenir une norme dans les nouveaux projets. 

Liens utiles :