RETROSPECTIVE

July 31st, 2021

Building a GraphQL React Prototype

GraphQL

React

JavaScript

AWS

Kubernetes

Docker

Nginx

Last year, I started using GraphQL at my job. I decided to create some GraphQL prototypes in my spare time, to get better acquainted with the GraphQL ecosystem. In 2018 I learned the basics of GraphQL and wrote two articles about my experience, but never dove into using GraphQL in real world applications. The GraphQL React prototype discussed in this article along with my Apollo prototype are the beginnings of that production application journey. In the future, I plan on using GraphQL for the API layer of my applications.

The GraphQL prototype discussed in this article is a React front-end application that connects to a GitHub GraphQL API. The API provides details about my repositories, and React displays those details in a dashboard. The dashboard is shown below.

The dashboard has a responsive design, so its also viewable on tablets and mobile screens.

The React code for the dashboard is broken down into multiple components, with each box displayed on the screen being its own component. Components use Less for their stylesheet language. Less, similar to Sass, is a CSS preprocessor, which adds features on top of the base CSS language.

Each component makes a GraphQL query to the GitHub API, collecting the data it wishes to display. The front-end code doesn't use a GraphQL client library to make the API calls, instead just using an HTTP client library called axios. In an upcoming article I'll discuss my Apollo prototype, which uses the Apollo Client library to make API calls. While the HTTP client library is a bare-bones approach, it is still fully capable of handling GraphQL APIs.

Let's look at one of the basic components displayed on the UI. The RepositoryCount component has the responsibility of displaying the number of repositories in my GitHub account on the dashboard. It has a RepositoryCount.js file for the React component code and a RepositoryCount.less file for the stylesheet code.

The RepositoryCount component makes a GraphQL API call to the GitHub API after the component first renders. Once a result is returned from the API, the component re-renders with the number of repositories in my GitHub account.

import React, { useEffect, useState } from 'react'; import { getPersonalRepositories } from '../../datasource/GraphQL'; const RepositoryCount = () => { const [repoCount, setRepoCount] = useState(0); const [error, setError] = useState(null); useEffect(() => { async function getGraphQLResult() { const result = await getPersonalRepositories('AJarombek'); if (result.data.data) { setRepoCount(result.data.data.user.repositories.totalCount); setError(null); } else { setError(result.data.errors[0].message); } } getGraphQLResult(); }, []); return ( <div className="items repository-count"> {error ? ( <div className="error"> <h6>{error}</h6> </div> ) : ( <> <h3>Number of Repositories</h3> <h2>{repoCount}</h2> </> )} </div> ); }; export default RepositoryCount;

The repository count is held in a useState React hook with the name repoCount. This value is changed using the setRepoCount function. When setRepoCount is invoked, the RepositoryCount component re-renders and displays the repository count in an HTML <h2> element. If an error occurs while making the API call, the error message is stored in the error variable, which is also managed by a useState React hook. If the error variable contains a string value, it is displayed in an HTML <h6> element.

The useEffect React hook is invoked once after the component first renders. It's purpose is to make the GitHub API call and store its response in a state variable. The getGraphQLResult() function helps performs this task. The first line of getGraphQLResult() calls a getPersonalRepositories() function, which is imported from another file. I extracted all the API client logic into a separate file, which I will discuss later. The API call is asynchronous, which is why I use the await keyword. getPersonalRepositories() returns a JavaScript object with the API response, and the remainder of the logic in getGraphQLResult() handles the response in the case of a success or failure.

If the API call is successful and the response object contains data, the repository count is assigned to the repoCount variable using the setRepoCount function. If the API call is unsuccessful and contains no data, the API returns an error message. This error message is assigned to the error variable using the setError function.

As previously mentioned, the API client logic is encapsulated in a separate file called GraphQL.js. getPersonalRepositories(), which is used in the RepositoryCount component, is defined as follows:

const getPersonalRepositories = (username) => request(getPersonalRepositoriesQuery, { username });

getPersonalRepositories() takes a GitHub username as an argument (in my case AJarombek) and then invokes a request() function. request() takes two arguments: a GraphQL query string and a JavaScript object containing variables passed to the GraphQL query. request() uses the axios HTTP client library to make the GraphQL API request to the GitHub API.

import axios from 'axios'; const instance = axios.create({ baseURL: 'https://api.github.com/graphql', headers: { Authorization: `bearer ${process.env.GITHUB_ACCESS_TOKEN}`, }, }); const request = (query, variables) => instance.post('', { query, variables });

As you can see, both the GraphQL query and the query variables are passed in an HTTP POST body to https://api.github.com/graphql.

The final piece of the puzzle is the GraphQL query. The first argument to request() in the getPersonalRepositories() function body was a variable named getPersonalRepositoriesQuery. This variable is a string containing a GraphQL query.

const getPersonalRepositoriesQuery = ` query PersonalRepositories($username: String!) { user(login: $username) { repositories(isFork: false, isLocked: false, privacy: PUBLIC, affiliations: OWNER, ownerAffiliations:OWNER, first: 100) { totalCount } } } `;

All the code I've shown, from the RepositoryCount component to the axios client, executes a GraphQL query and renders the result in a web browser.

For additional perspective, shown below is the GraphQL query executed from an HTTP UI client. In this case I'm using Insomnia, but you can use Postman or any other client for HTTP requests. The important point is that GraphQL queries aren't doing anything too complex, they are simply HTTP requests.

Let's briefly look at one more component. The TotalCommits component displays the five repositories with the most code commits in my GitHub account. TotalCommits has a TotalCommits.js file for the React component code and a TotalCommits.less file for the stylesheet code.

TotalCommits is a bit more complex because it needs to manipulate the data received from the GraphQL API, and then display the data in a list.

import React, { useEffect, useState } from 'react'; import { getTotalCommits } from '../../datasource/GraphQL'; const TotalCommits = () => { const [repoCommits, setRepoCommits] = useState([]); const [error, setError] = useState(null); const generateMostTotalCommits = (repositories) => { const repositoriesByCommits = []; for (const repository of repositories) { repositoriesByCommits.push({ name: repository.node.name, commits: repository.node.ref?.target?.history?.totalCount ?? 0, }); } repositoriesByCommits.sort((a, b) => b.commits - a.commits); setRepoCommits(repositoriesByCommits.slice(0, 5)); setError(null); }; useEffect(() => { async function getGraphQLResult() { const result = await getTotalCommits('AJarombek'); if (result.data.data) { generateMostTotalCommits(result.data.data.user.repositories.edges); } else { setError(result.data.errors[0].message); } } getGraphQLResult(); }, []); return ( <div className="items total-commits"> {error ? ( <div className="error"> <h6>{error}</h6> </div> ) : ( <> <h2>Most Total Commits</h2> { repoCommits.map((repository) => ( <div className="commits" key={repository.name}> <p>{repository.name}</p> <p>{repository.commits}</p> </div> ))} </> )} </div> ); }; export default TotalCommits;

In many ways, TotalCommits is structured the same way as RepositoryCount. It renders either an error message or data from the GitHub API. It utilizes useState() and useEffect() React hooks to retrieve and store the API data after the components initial render. There are two main things that differentiate TotalCommits from RepositoryCount. First, it restructures and sorts the data retrieved from the API in a separate function called generateMostTotalCommits(). After this function runs, an array of five JavaScript objects are stored in a repoCommits variable, representing the five repositories in my GitHub account with the most commits. Second, the component renders each of these repositories in their own HTML <div> element, displaying the repository name and the number of commits.

TotalCommits invokes a getTotalCommits function to retrieve the repository information from the GitHub GraphQL API. This function exists in the GraphQL.js file. The GraphQL query for this component is shown below, invoked from the Insomnia HTTP UI Client.

All the components in my GraphQL React prototype follow a similar structure to TotalCommits and RepositoryCount. The rest can be viewed on GitHub.

The GraphQL React prototype uses Kubernetes infrastructure hosted on AWS. This infrastructure is built with Terraform, and has the following structure:

There are three Terraform modules for the infrastructure: acm for ACM certificates, ecr for ECR repositories containing Docker images, and k8s for the Kubernetes infrastructure. They exist in acm, ecr, and k8s directories, respectively. The Terraform infrastructure is built and torn down in Jenkins pipelines, specifically create-acm-infrastructure, create-ecr-infrastructure, create-k8s-infrastructure, destroy-acm-infrastructure, destroy-ecr-infrastructure, and destroy-k8s-infrastructure.

The Kubernetes infrastructure for the GraphQL React prototype is built with two Docker images. Their Dockerfiles are defined in the GitHub repository. The first image is a base image, containing all the npm dependencies needed by the React application. This Docker image is defined in a base.dockerfile file.

FROM node:14.4.0 COPY . app WORKDIR app RUN npm install

The second image is an application image. It builds the React application and creates an nginx proxy to route traffic entering the container to the application. This Docker image is defined in a app.dockerfile file.

FROM 739088120071.dkr.ecr.us-east-1.amazonaws.com/graphql-react-prototype-base:latest AS base WORKDIR app RUN npm run build FROM nginx AS host RUN rm /etc/nginx/conf.d/default.conf COPY nginx.conf /etc/nginx/conf.d COPY --from=base /app/dist /usr/share/nginx/html

The nginx proxy has the following configuration, defined in a nginx.conf file.

server { listen 80; location / { root /usr/share/nginx/html; try_files $uri /index.html; } }

I don't discuss the infrastructure any further in this article, but I have lots of articles on Terraform, AWS, Kubernetes, Docker, and Jenkins if you want to learn more about the underlying technologies.

The GraphQL React prototype has unit tests and snapshot tests with full code coverage. Tests exist in the test directory and are written with Jest. I wrote an article about writing integration, unit, and snapshot tests for React with Jest if you want to learn more.

Jest unit and snapshot tests for the GraphQL React prototype are run in Jenkins on a daily schedule. The Jenkins pipeline code for these tests exists in my global-jenkins-jobs repository.

Creating this GraphQL React prototype was a great exercise and helped me learn how to use GraphQL in a frontend application. However, I likely won't repeat the technology choice of using an HTTP client library instead of a GraphQL client library for front-ends dealing extensively with GraphQL APIs. In many ways, I made things more difficult for myself by using axios instead of a GraphQL library. I have another prototype application which uses the apollo GraphQL client library with React, and it made integrating GraphQL with React quite a bit easier.

All of the application code discussed in this article is available on GitHub.