loading...
Cover image for How to Build an Automated Portfolio using GitHub's GraphQL API and React

How to Build an Automated Portfolio using GitHub's GraphQL API and React

imjoshellis profile image Josh Ellis Updated on ・8 min read

First of all, thanks for all the love on last week's post! I mentioned I'd do a followup if there as interest, so here we are for part 2.

Last week was an overview of how I'm using GitHub's API to pull data from the projects I'm working on to automate my website. If you didn't read it, don't worry, this tutorial is standalone, but you may want to go read the other post afterward.

Getting Started

Here's what we'll be building: live demo. The repo is located here on GitHub. I also set up a code sandbox if you prefer.

Note: The code sandbox will NOT work unless you add a .env file with your GH token in it (see below). I recommend you make a private fork in order to do so!

As you can see, the styling will be minimal. I'll leave customization up to you to suit your style/needs.

To make this easy to follow, I'll be starting from scratch with create-react-app. TypeScript plays nicely with GraphQL, so I'll be using the TS template.

Create React App

npx create-react-app graphql-portfolio --template typescript

Install Dependencies

For this project, we'll need the following packages:

yarn add graphql graphql-tag urql dotenv

And these dev packages:

yarn add -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-urql

What Did We Just Install?

codegen is a cli (command line interface) that generates hooks for us from graphql files (more on that later).

graphql / graphql-tag are required at runtime by the hooks that codegen will generate.

dotenv is used to load our GitHub authorization token into our requests to the API.

urql is the GraphQL client that we'll be using to communicate with GitHub's API.

urql vs Apollo (GraphQL Clients)

I'm still figuring all this GraphQL stuff out too, so I can't comment in depth on what situations each client would be better for.

I've used both, and I actually used Apollo on my portfolio. The only reason I chose urql here is because I've been using it a lot for another project of mine, so I'm more fluent with the workflow right now.

Codegen Setup

To get codegen working, we need to setup a config file and add a script to package.json.

Let's start with the config. Create a new file called codegen.yml in the same root directory as package.json with the following:

overwrite: true
schema:
  - https://api.github.com/graphql:
      headers:
        Authorization: 'Bearer ${REACT_APP_GH_TOKEN}'
documents: 'src/graphql/**/*.graphql'
generates:
  src/generated/graphql.tsx:
    plugins:
      - 'typescript'
      - 'typescript-operations'
      - 'typescript-urql'

As you can see, we're telling Codegen the API address, auth info, the directory where we'll put our .graphql files, where it should put its generated file, and which plugins we're using.

We'll set up the REACT_APP_GH_TOKEN in a bit.

Now that that's done, let's add the script to package.json

// package.json
{
  "scripts": {
    /* ...Create-React-App Default Scripts, */
    "gen": "graphql-codegen -r dotenv/config --config codegen.yml"
  },
}

Now we'll be able to run yarn gen in the terminal to generate our hooks/types file.

Finally, you need to set up an access token with GitHub. Follow the steps here to get your token and come back: Creating a personal access token

EDIT: I just learned I misunderstood how .env works clientside. I'm currently researching better ways to work with private keys on public frontend apps. Suggestions are welcome. In the meantime, make sure you only allow read access on the token you create!

That token will go in a new file called .env in your root directory with package.json & codegen.yml:

# .env
REACT_APP_GH_TOKEN='[TOKEN HERE]'

We'll access that value when running yarn gen and also when using urql to run queries.

Note: Be sure to add .env to your .gitignore file! You don't want that token shared publicly!

And with that, we've done all the basic set up!

Your First GraphQL Query

Ok, time to take a break from your terminal/IDE and head over to the GitHub GraphQL Explorer and sign in with GitHub.

The starter query looks like this:

query {
  viewer {
    login
  }
}

Press the 'play' button to see the response, and let's break it down, starting with the query.

Anatomy of a GraphQL Query

The first word query is a GraphQL keyword. The other option here would be mutation. The difference is in the names: a query only gets access to data, while a mutation is able to send data that the server can work with.

If you're familiar with REST API terms, you can think of query as a GET and mutation as similar to POST/PATCH.

Next, we see viewer. In GitHub's API, this refers to the authenticated User -- aka you! That's the token will be for later when we implement a query in React.

Finally, inside the viewer, we need to specify what we want the API to give us in return. In this case, login returns your GitHub username.

Anatomy of a GraphQL Response

If you pressed the play button to run the query, you'll see the response in the right area. One of the awesome things about GraphQL is the response mirrors your query as a JS/TS object (no need to deal with JSON methods).

Let's see what happens if you don't query any fields on viewer. The explorer won't let you run this query:

query {
  viewer
}

It will automatically change the query to this:

query {
  viewer {
    id
  }
}

The explorer keeps us from hitting errors, but if you ran the query without a field on viewer, you'd get an error response from the server because it expects you to ask for fields (otherwise, it can't give you anything in response!).

Building Our Query

For this project, we'll be grabbing your top three pinned repositories. Test out the following in the explorer:

query PinnedRepos {
    viewer {
      pinnedItems(first: 3) {
        edges {
          node {
            ... on Repository {
              name
              description
            }
          }
        }
      }
    }
  }

This is a named query. The only purpose of PinnedRepos is to give us a name to reference later. The sever doesn't care about what comes between query and the first {.

The first new line -- pinnedItems(first: 3) -- gets your pinned items. The part in parenthesis is a filter so the server only sends back the first 3 (since you can pin up to 6 repos).

Now, GitHub uses a complex pattern of edges and nodes. We won't go into detail on how that works exactly. Basically, edges is all the items (in this case, 3 pinned repos), and node is an individual item.

Next, we use ... on Repository to tell GitHub which fields we want. Right now, we're just asking for name and description. Hit the run button, and if you have pinned repos, you should see a response that mirrors the structure of our query.

To finalize the query, let's grab a few more fields:

query PinnedRepos {
    viewer {
      pinnedItems(first: 3) {
        edges {
          node {
            ... on Repository {
              name
              description
              pushedAt
              url
              homepageUrl
            }
          }
        }
      }
    }
  }

pushedAt is what it sounds like: the time of the most recent push.

url returns the repo's url

homepageUrl returns the homepage url (if available)

Back to React

Set up the graphql query

Now that our query is setup, let's head back to our files and add one: src/graphql/queries/PinnedRepos.graphql. Go ahead and paste the query in just as it is above.

Hit save, and now that we've got our query ready, you can run yarn gen in the terminal to make Codegen do its thing.

If all goes well, you should see a new generated file pop up in src/generated/graphql.tsx.

Set up the urql client

Now let's get urql up and running. Open App.tsx so we can initialize an urql client and wrap our app in a provider. Note: We haven't created the <PinnedRepos /> component yet, but we'll add it right after this.

import React from 'react'
import { createClient, Provider } from 'urql'
import './App.css'
import PinnedRepos from './components/PinnedRepos'

const client = createClient({
  url: 'https://api.github.com/graphql',
  fetchOptions: {
    headers: { authorization: `Bearer ${process.env.REACT_APP_GH_TOKEN}` }
  }
})

const App = () => (
  <Provider value={client}>
    <div className='App'>
      <h1>My Automated Portfolio</h1>
      <PinnedRepos />
    </div>
  </Provider>
)

export default App

We're not doing anything special in createClient other than adding our auth token. Every request you make will use the token so GitHub's server knows it's you asking for the data.

Create a simple <PinnedRepos /> component in scr/components/PinnedRepos.tsx to make sure everything's working:

import React from 'react'
import { usePinnedReposQuery } from '../generated/graphql'

export const PinnedRepos: React.FC = () => {
  const [{ data }] = usePinnedReposQuery()
  console.log(data)
  return <>{data ? <p>Loaded</p> : <p>Loading...</p>}</>
}

export default PinnedRepos

If you load up React on a local server by running yarn start, you should see 'Loading...' for a split second and then 'Loaded'. In your console, you'll see the data object, which should match the test query we did in the explorer:

{
  viewer: {
    pinnedItems: {
      edges: Array(3)
    }
  }
}

So then to display the data, we just need to map over the edges. To make things simple, I'm using inline JSX styles here. For a real website, I highly recommend using CSS or some kind of style library!

import React from 'react'
import { usePinnedReposQuery } from '../generated/graphql'

export const PinnedRepos: React.FC = () => {
  const [{ data }] = usePinnedReposQuery()
  return (
    <>
      {data?.viewer.pinnedItems.edges ? (
        <div
          style={{
            display: 'flex',
            flexDirection: 'row',
            justifyContent: 'center',
            textAlign: 'left'
          }}
        >
          {data.viewer.pinnedItems.edges.map((node, index) => {
            if (node && node.node) {
              const { name, description, url, homepageUrl, pushedAt } = {
                name: '',
                description: '',
                url: '',
                homepageUrl: '',
                pushedAt: '',
                ...node.node
              }
              return (
                <div
                  key={index}
                  style={{ marginLeft: '1rem', maxWidth: '24rem' }}
                >
                  <h2>{name}</h2>
                  {pushedAt ? <p>updated: {pushedAt}</p> : null}
                  <h4 style={{ marginBottom: 0 }}>Description</h4>
                  <p style={{ marginTop: 0 }}>
                    {description ? description : 'no description'}
                  </p>
                  <a href={url}>View on GitHub</a>
                  {homepageUrl ? (
                    <a href={homepageUrl} style={{ marginLeft: '1rem' }}>
                      View website
                    </a>
                  ) : null}
                </div>
              )
            } else {
              return null
            }
          })}
        </div>
      ) : (
        <p>Loading...</p>
      )}
    </>
  )
}

export default PinnedRepos

And that's it! You now have a minimal React app that's using data from your GitHub pinned repos. What you do with that data (or other data you might query) is totally up to you, so I'll leave you with this. Check out last week's post to see some of the other queries I'm using on my portfolio.

Resources

Let's Talk

If you have any questions, leave a comment, and I'll do my best to answer it! Also, I'm still learning GraphQL, so please let me know if I included any misinformation.

Thanks for reading!

Posted on by:

imjoshellis profile

Josh Ellis

@imjoshellis

he/him • Crafting products that simplify the human experience. I'm passionate about innovative tech in productivity 📋️, mental health 🧘‍♂️️, and travel 🏕️.

Discussion

pic
Editor guide
 

There is plenty more to talk about when it comes to GraphQL. What did I leave out? What do you want to learn about?

Maybe I can write about creating a GraphQL server next?