Shada
Posted on August 26, 2021
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
- yarn/npm - vlatest
- Strapi - vlatest
- 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
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
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": []
}
}
]
}
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;
}
},
};
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;
}
},
};
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",
},
},
},
};
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
}
}
}
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>
);
}
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;
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;
Your final site should look more or less like so:
Source code
The source code for this article can be found below:
- Backend - https://github.com/RazvanCretu/backend
- Frontend - https://github.com/RazvanCretu/frontend
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.
Posted on August 26, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.