Custom Routes for External Data with GraphQL

Shada - Aug 26 '21 - - Dev Community

Introduction

In case you are a gamer and an avid programmer too. You've come in the right spot. We will try to reproduce a 1$ version of the popular gaming stats site https://eune.op.gg/.

Along the way, in this article, you will learn how to create a custom API endpoint with Strapi. For the endpoint, a custom action will be defined too to be able to fetch external data. We will also enable the GraphQL plugin and allow the custom API endpoint to query through GraphQL with custom schema and resolvers.

You can download the backend here: Github repository, and we will integrate it with a NextJs frontend project to display our gaming statistics nicely.

What is Graphql?

As stated on the official GraphQL page, it is nothing more than a query language for APIs. GraphQL allows a client (frontend page) to retrieve only the data it needs from an API and nothing more, making it a faster and scalable way to power complex applications.

It was firstly developed by Facebook and became open-source in 2015.

Prerequisites

  1. yarn/npm - vlatest
  2. Strapi - vlatest
  3. NextJs - vlatest

Before starting on building the app, make sure that you already have a Riot Games account. If not, you can easily create one or use the social login button. You will need an account to be able to generate an API key on the dashboard page.

Once you have generated the key, make sure to store it in a .env file so it is not publicly exposed inside the code, and keep it secret.

Building the app

Now that everything is set up let's get our hands dirty and start on coding. The first thing you would want to do is pick an easy and accessible location to store our backend/frontend projects. Mine will reside under C:\Projects.

Make sure to create the frontend and backend folders first.

Backend set-up

For the backend part, let's begin with a simple Strapi quickstart application.

Open a terminal window and navigate under C:\Projects\backend. Once in the desired location, run the following command:

    yarn create strapi-app <name> --quickstart
Enter fullscreen mode Exit fullscreen mode

It will create all the files and configurations needed to start coding once the project is created directly. The default database that will be used is SQLite.

As required, make sure to add the following packages after the installation is complete yarn add axios strapi-plugin-graphql. The latter one will download and install the GraphQL plugin for your Strapi app.

Once you've created your admin profile and managed to log in to the admin page: http://localhost:1337/admin, the next thing we will be creating the custom API. To do so, we will be using the Strapi CLI capabilities, or you can do it manually if you prefer so.

Make sure that you are located under the root directory of your backend project, and inside your terminal, run the following command:

    strapi generate:api riot
Enter fullscreen mode Exit fullscreen mode

For this article, I won't be needing the model's folder so you could delete it and all its content under .\backend\api\riot\models. This way, it won't show up on the sidebar of the admin panel as we don't necessarily need it there for now.

Custom routes

Now let's start creating our custom route. Inside the following file .\api\riot\config\routes.json, you can overwrite all the generated routes with the following code:

    {
      "routes": [
        {
          "method": "GET",
          "path": "/summoner/:summoner",
          "handler": "summoner.findSummonerByName",
          "config": {
            "policies": []
          }
        }
      ]
    }
Enter fullscreen mode Exit fullscreen mode

Each time a request is sent to the endpoint localhost:1337\summoner\:summoner, the controller handler action findSummonerByName will be called.

Controller

Now let’s define our custom controller action. Make sure to copy the following bits of code inside .\api\riot\controllers\summoner.js:

    "use strict";
    module.exports = {
      findSummonerByName: async (ctx) => {
        try {
          const summoner = ctx.params.summoner || ctx.params._summoner;
          const profile = await strapi.services.riot.summoner(summoner);
          const games = await strapi.services.riot.games(profile.puuid);
          return { ...profile, games: games };
        } catch (err) {
          return err;
        }
      },
    };
Enter fullscreen mode Exit fullscreen mode

Please take note that inside our controller, we're making use of two custom services. These services are created to fetch the required data from the Riot servers and return it to our API. You can also make use of them anywhere you want inside your Strapi application.

Services

Now let's take a look at our services code that is under .\api\riot\services\riot.js

    const axios = require("axios");

    const fetchRiot = async (uri) => {
      const { data } = await axios.get(uri, {
        headers: { "X-Riot-Token": process.env.RIOT_KEY },
      });
      return data;
    };

    module.exports = {
      summoner: async (name) => {
        // Setup e-mail data.
        try {
          const data = await fetchRiot(
            `https://eun1.api.riotgames.com/lol/summoner/v4/summoners/by-name/${name}`
          );
          return data;
        } catch (err) {
          return err;
        }
      },
      games: async (puuid) => {
        try {
          const data = await fetchRiot(
            `https://europe.api.riotgames.com/lol/match/v5/matches/by-puuid/${puuid}/ids?start=0&count=5`
          );
          const games = await Promise.all(
            data.map(async (id) => {
              const {
                info: {
                  gameCreation,
                  gameDuration,
                  gameId,
                  gameMode,
                  participants,
                },
              } = await fetchRiot(
                `https://europe.api.riotgames.com/lol/match/v5/matches/${id}`
              );
              return {
                gameCreation: gameCreation,
                gameDuration: gameDuration,
                gameId: gameId,
                gameMode: gameMode,
                ...participants.filter((item) => {
                  return item.puuid == puuid;
                })[0],
              };
            })
          );
          return games;
        } catch (err) {
          return err;
        }
      },
    };
Enter fullscreen mode Exit fullscreen mode

GraphQL schema

The Strapi CLI does not automatically generate the following file that we will need, but you can manually create it with no worries.

So inside .\api\riot\config\schema.graphql.js is the place where you will need to define the custom GraphQL schema for your custom routes. Make sure to have to following code pasted inside:

    module.exports = {
      definition: `
          type Game {
            gameCreation: Int!,
            gameDuration: Int!,
            gameId: Int!,
            gameMode: String!,
            assists: Int!,
            kills: Int!,
            deaths: Int!,
            championName: String!,
            champLevel: Int!
            win: Boolean!
          }

          type Summoner {
            id: String!,
            accountId: String!,
            puuid: String!, 
            name: String!,
            profileIconId: Int!,
            revisionDate: Int!,
            summonerLevel: Int!,
            games: [Game]
          }`,
      query: `
            Summoner(summoner: String!): Summoner!
          `,
      resolver: {
        Query: {
          Summoner: {
            description: "Get the Summoner object in the Riot API.",
            resolver: "application::riot.summoner.findSummonerByName",
          },
        },
      },
    };
Enter fullscreen mode Exit fullscreen mode

Now that everything is in place in your browser, you can navigate to the GraphQL interface: http://localhost:1337/graphql of your project and test the following query:

    query SummonerByName($summoner: String!){
            SummonerInfo: Summoner(summoner: $summoner ) {
            id
            puuid 
            name
            summonerLevel
            profileIconId
            games {
                    gameMode
                    gameCreation
                    gameDuration
                    assists
                    kills
                    deaths
                    championName
                    champLevel
                    win
                  }
            }
    }
Enter fullscreen mode Exit fullscreen mode

Make sure that you also set the summoner variable with your account's summoner name or any existing summoner name if you prefer.

If you managed to follow along correctly, it should return all the requested data successfully.

Take note that for our custom GraphQL Types we only declared less fields than returned from our REST endpoint. That’s the power of GraphQL as it can return only the required fields as oppose to the REST endpoint that returns all the fields available. This way your frontend site won’t be loaded with unnecessary data.

Make sure that you also have a look over the Strapi documentation on customizing the GraphQL schema.

Frontend Setup

Awesome! Now that our backend is ready, let's jump straight into our frontend page.

For the frontend page, we will be using NextJs. You can start a new project by running the following command yarn create next-app <name> under C:\Projects\frontend, and it will automatically bootstrap all the files and configurations needed so you can start on coding.

After the installation is done, make sure to add the following dependencies that we will use through the application yarn add @apollo/react-hooks @emotion/react @emotion/styled apollo-boost axios graphql.

Home page

We will be using the already generated structure for the home page to render our main part of the app.

As a first step, into .\frontend\pages\index.js you can simply copy and paste the below code:

    import Head from "next/head";
    import Image from "next/image";
    import styles from "../styles/Home.module.css";
    import styled from "@emotion/styled";
    import Query from "../components/Query";
    import SUMMONER_QUERY from "../queries/summoner/summoner";

    const Summoner = styled.div`
      flex-direction: column;
      p {
        padding: 0;
        margin: 0;
        line-height: 1.5;
      }
      img {
        margin-bottom: 10px;
        border-radius: 50px 50px;
      }
    `;
    const Container = styled.div`
      display: flex;
      justify-content: center;
      padding-top: 35vh;
    `;
    const Stats = styled.div`
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
    `;
    export default function Home() {
      return (
        <div className={styles.container}>
          <Head>
            <title>Create Next App</title>
            <meta name="description" content="Generated by create next app" />
            <link rel="icon" href="/favicon.ico" />
          </Head>
          <Container>
            <main className={styles.main}>
              <Query query={SUMMONER_QUERY} summoner="TwistedPot">
                {({ data: { SummonerInfo } }) => {
                  return (
                    <div>
                      <Summoner className={styles.grid}>
                        <img
                          src={`http://ddragon.leagueoflegends.com/cdn/11.15.1/img/profileicon/${SummonerInfo.profileIconId}.png`}
                          alt="Image"
                          height="100"
                          width="100"
                        />
                        <p>{SummonerInfo.name}</p>
                        <p>Level: {SummonerInfo.summonerLevel}</p>
                      </Summoner>
                      <div className={styles.grid}>
                        {SummonerInfo.games.map((game) => {
                          return (
                            <div className={styles.card} key={game.gameCreation}>
                              <Stats>
                                <div
                                  style={{
                                    display: "flex",
                                    flexDirection: "row",
                                    justifyContent: "center",
                                    alignItems: "center",
                                  }}
                                >
                                  <img
                                    src={`http://ddragon.leagueoflegends.com/cdn/11.15.1/img/champion/${game.championName}.png`}
                                    alt="Image"
                                    height="50"
                                    width="50"
                                    style={{
                                      borderRadius: "50px 50px",
                                    }}
                                  />
                                  <p
                                    style={{
                                      paddingLeft: "25px",
                                    }}
                                  >
                                    {game.championName}
                                  </p>
                                </div>
                                <div
                                  style={{
                                    display: "flex",
                                    flexDirection: "row",
                                    justifyContent: "center",
                                    alignItems: "center",
                                    padding: "10px 0px",
                                  }}
                                >
                                  <img
                                    src={`http://ddragon.leagueoflegends.com/cdn/5.5.1/img/ui/score.png`}
                                    alt="Image"
                                    height="25"
                                    width="25"
                                  />
                                  <p>
                                    {game.kills +
                                      "/" +
                                      game.deaths +
                                      "/" +
                                      game.assists}
                                  </p>
                                </div>
                                <div
                                  style={{
                                    display: "flex",
                                    flexDirection: "column",
                                    width: "100%",
                                  }}
                                >
                                  <p>Champion Level: {game.champLevel}</p>
                                  <p>Mode: {game.gameMode}</p>
                                  <p>
                                    Duration:{" "}
                                    {Math.round(game.gameDuration / 1000 / 60)}{" "}
                                    minutes.
                                  </p>
                                  <p>Result: {game.win ? "Win" : "Lose"}</p>
                                </div>
                              </Stats>
                            </div>
                          );
                        })}
                      </div>
                    </div>
                  );
                }}
              </Query>
            </main>
          </Container>
          <footer className={styles.footer}>
            <a
              href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
              target="_blank"
              rel="noopener noreferrer"
            >
              Powered by{" "}
              <span className={styles.logo}>
                <Image src="/vercel.svg" alt="Vercel Logo" width={72} height={16} />
              </span>
            </a>
          </footer>
        </div>
      );
    }
Enter fullscreen mode Exit fullscreen mode

If you try to run the project with yarn dev, you will probably get some errors. That's because to continue, we will need to create a couple more things.

Query component

The first thing is to create a reusable Query component that we can use anywhere in our code to fetch the \graphql endpoint. The inspiration for the Query component was drawn from this blog post.

So Maxime deserves all the credits that I could stay DRY.

Make sure to have the following file created .\frontend\components\Query\index.js

    import React from "react";
    import { useQuery } from "@apollo/react-hooks";
    const Query = ({ children, query, summoner }) => {
      const { data, loading, error } = useQuery(query, {
        variables: { summoner: summoner },
      });
      if (loading) return <p>Loading...</p>;
      if (error) return <p>Error: {JSON.stringify(error.message)}</p>;
      return children({ data });
    };
    export default Query;
Enter fullscreen mode Exit fullscreen mode

It has been slightly modified so that it matched our needs.

The Query

The second thing that we will be needed is the actual query that we will use inside the Query component. You can use the GraphQL Interface that is available under http:\localhost:1337\graphql to build the query.

For the query, make sure to create the following file .\frontend\queries\summoner\summoner.js

    import gql from "graphql-tag";
    const SUMMONER_QUERY = gql`
      query SummonerByName($summoner: String!) {
        SummonerInfo: Summoner(summoner: $summoner) {
          id
          puuid
          name
          summonerLevel
          profileIconId
          games {
            gameMode
            gameCreation
            gameDuration
            assists
            kills
            deaths
            championName
            champLevel
            win
          }
        }
      }
    `;
    export default SUMMONER_QUERY;
Enter fullscreen mode Exit fullscreen mode

Your final site should look more or less like so:

Source code

The source code for this article can be found below:

Conclusion

Congratulations for making it this far!

By the end of this tutorial, you should understand how to easily create custom routes to access external data and create a custom GraphQL schema for your routes to match your business logic.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .