How to Deploy an Automated Portfolio with Netlify & Netlify's Serverless Functions
Tags: graphql, serverless, typescript, tutorial
Published: 2020-09-28 17:00:37

This is probably the last post in my automated portfolio series. Last week, we went over how to implement urql in React to make GraphQL calls to the GitHub GraphQL API.

In this final installment, we'll go over how to actually deploy the site using Netlify, and how you can keep your API key private using Netlify "serverless" functions.

I'll be picking up from the code of [last week's post](https://dev.to/imjoshellis/how-to-build-an-automated-portfolio-using-github-s-graphql-api-and-react-5bii). Here's what we'll be building: [live demo](https://graphql-portfolio.netlify.app/). You can find this week's final code on the [deploy branch](https://github.com/imjoshellis/GraphQL-Portfolio/tree/deploy). I won't be merging it into master so you can make a fork of the master to follow along with this post.

Huge shoutout to [nomadoda on GitHub](https://github.com/nomadoda) because I learned a lot from how they set their Netlify functions up in [this repository](https://github.com/nomadoda/anima-dk).

## Set Up On Netlify

I won't go over how to get setup with Netlify in depth because their [docs](https://docs.netlify.com) cover everything you'll need:

1. First, you'll want to [create an account](https://app.netlify.com/signup).
2. Then, optionally, you can [install the Netlify CLI](https://docs.netlify.com/cli/get-started/#installation).
3. If you haven't already, create a GitHub repository for your project. To follow along, I recommend [forking the repo from last week's post](https://github.com/imjoshellis/GraphQL-Portfolio) since that's the code I'll be using.
4. Finally, log into your account, click "New Site From Git", and follow the prompts to get your site up and running.

If you did everything right, when you go to the site that Netlify deployed, you should see "Loading..." and if you open your console, there should be an error.

That's because you don't have an environment variable set up yet. We'll get to that later.

## Set Up the Build Process

Let's switch gears now and open up the project locally. Clone it if you need to, then in the root folder of the project, create a file called `netlify.toml` with this code:

```toml
[build]
  command = "yarn run build"
  functions = "functions/dist/functions"
```

This tells Netlify what to do to build the app and where to look for functions.

If you installed the Netlify CLI, you can run `netlify dev` to start up a local server and make sure everything is working. You should see the same page as you deployed with a with "Loading..." that never resolves.

Now, create a folder called `functions` in the root directory.

In `functions`, create a new `package.json`:

```json
// functions/package.json
{
  "name": "functions",
  "version": "1.0.0",
  "description": "",
  "scripts": {
    "build": "npm install && npm run clean && tsc",
    "build:watch": "npm run clean && tsc-watch",
    "clean": "rimraf dist"
  },
  "author": "",
  "license": "",
  "dependencies": {
    "apollo-server-lambda": "^2.18.1",
    "axios": "^0.20.0",
  },
  "devDependencies": {
    "@types/node": "^13.11.0",
    "rimraf": "^3.0.2",
    "tsc-watch": "^4.2.3",
    "typescript": "^3.8.3"
  }
}
```

Creating a `package.json` within the functions folder allows you to keep your function dependencies separate from your React dependencies.

If you're curious what each dependency is for...
    - `apollo-server-lambda` is how we'll set up a graphql endpoint
    - `axios` makes querying the GitHub API simple
    - `@types/node`, `tsc-watch`, and `typescript` are for TypeScript support
    - `rimraf` is used to clear the `dist` folder when building

There are also some important scripts:

- `clean` uses `rimraf` to delete the `dist` folder
- `build` runs an install, then `clean` (see above), and finally `tsc` which will compile TypeScript files into the `dist` folder

Next, we need to modify the root folder's `package.json`:

```json
// package.json
{
  // ...
  "scripts": {
    // ...
    "build": "concurrently \"npm run build:web\" \"npm run build:functions\"",
    "build:functions": "cd functions && npm run build",
    "build:web": "react-scripts build",
  },
  "devDependencies": {
    // ...
    "concurrently": "^5.1.0"
  }
}
```

As you can see, we've changed the original `build` script to `build:web` and added a new `build:functions` script that runs the `build` in `/functions`.

Using `concurrently` in the main `build` script allows the process to go a bit faster since it will run both builds at the same time.

Lastly, in your `.gitignore` file, change `/node_modules` to `node_modules` without the slash because if you build `/functions`, you'll create `/functions/node_modules` and we don't want that folder tracked by git.

That's all the node setup done. Let's setup Typescript next.

## Setting up TypeScript for Functions

We've already got all the dependencies we need setup from the last section, but we do need to make a `tsconfig.json` in `/functions`:

```json
// functions/tsconfig.json
{
  "compilerOptions": {
    "target": "es2019",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "module": "commonjs",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "outDir": "./dist"
  },
  "include": ["src"]
}
```

This is a pretty basic `tsconfig`, so I won't go over each option. Feel free to ask questions in the comments!

Next, we'll need `functions/.gitignore`:

```bash
# functions/.gitignore
dist
```

If you compile the TypeScript files, it will generate JavaScript files in `dist`, and it's generally good to ignore generated/built files. You could instead put `/functions/dist` or simply `dist` in your root-level `.gitignore` if you prefer.

Your `/functions` should look like this right now:

```
 📂️ functions
 ┣ 📜 .gitignore
 ┣ 📜 tsconfig.json
 ┗ 📜 package.json
```

Add a folder called `src` with `index.ts` inside. Then add a folder called `functions` inside `src` and a folder called `github-graphql` inside `functions`. In there, create three files: `github-graphql.ts`, `schema.ts`, and `queries.ts`:

```
 📂️ functions
 ┣ 📂 src
 ┃ ┗ 📜 index.ts
 ┃ ┗ 📂 functions
 ┃ ┃ ┗ 📂 github-graphql
 ┃ ┃ ┃ ┗ 📜 github-graphql.ts
 ┃ ┃ ┃ ┗ 📜 schema.ts
 ┃ ┃ ┃ ┗ 📜 queries.ts
 ┣ 📜 .gitignore
 ┗ 📜 package.json
```

To be honest, nesting in a `functions` folder is a bit overkill here. But if we needed some shared code between functions (in a more complex app), we could put the shared code in the `src` folder without exposing them as functions.

Lastly, in all the `.ts` files, let's put a single line of useless code for now (just to make sure the build process is working):

```ts
// *.ts
export default null
```

If you have empty `.ts` files, the compiler will complain, but now with a simple `export`, you should be able to run `yarn install` and `yarn build` in the root folder without errors.

## How Netlify Functions Work

Let's start by talking about how Netlify functions work in the first place. Remember how in `netlify.toml` we set `functions = "functions/dist/functions"`? That tells Netlify where to look for our functions. What it will find in there is this structure from the TypeScript build:

```
 📂️ functions/dist/functions
 ┗ 📂 github-graphql
   ┗ 📜 github-graphql.js
   ┗ 📜 schema.js
   ┗ 📜 queries.js
```

When Netlify's parser sees a folder (`github-graphql`) in the functions folder, it looks for either `index.js` or a `.js` file matching the folder name (`github-graphql.js` in our case) to use as the 'main' file.

It then expects an exported function named `handler` to be present in the file that it finds.

### A Note on using `index.js`

Though it would be arguably look 'cleaner' to use `index.js` instead of duplicating the name of the folder, I find it's easier working in an IDE when the file has a unique name, which is why we're using `github-graphql` here as the file name instead of `index`.

## GraphQL Hello World with Netlify Functions

We'll start by seeing if we can get a simple 'hello world' query running. In `functions/src/functions/github-graphql/index.ts`, add this code:

```ts
// functions/src/functions/github-graphql/index.ts

import { ApolloServer } from 'apollo-server-lambda'
import typeDefs from './schema'

// First we set up a GraphQL `resolvers` object that has one `Query` which returns a promise that resolves the string `'hello world'`.
const resolvers = {
  Query: {
    helloWorld: async () => 'hello world'
  }
}

// Then, we create an Apollo server that uses the `resolvers` object and imported type definitions from `schema.ts` that we haven't set up yet.
const server = new ApolloServer({ typeDefs, resolvers })

// Finally, apollo-server-lambda conveniently has a .createHandler() method that we expose as the handler of our function.
export const handler = server.createHandler()
```

In `schemas.ts`, we set up the schema:

```ts
import { gql } from 'apollo-server-lambda'

export default gql`
  type Query {
    helloWorld: String
  }
`
```

This is to tell Apollo's GraphQL server that the `Query` called `helloWorld` should return a `String`. Note that unlike TypeScript, GraphQL types are capitalized.

Now let's set up the frontend to see if this is working. In
our queries folder, create a new file called `HelloWorld.graphql`:

```graphql
// src/graphql/queries/HelloWorld.graphql
query HelloWorld {
  helloWorld
}
```

While there, delete the `PinnedRepos.graphql` file. We'll be totally remaking it soon.

Next, we need to set up a `schema.gql` file for codegen in the root:

```graphql
// schema.gql
type Query {
  helloWorld: String
}
```

_Note: I'm sure there's a cleaner/DRY way of doing the schema instead of copying it in two places, but I wasn't able to figure out how to generate it automatically from the Netlify dev server and using `.graphql` files directly in `.ts` is complicated... I'll update here if I figure out a simple solution._

Now that we have a schema file, let's update `codegen.yml`:

```yml
overwrite: true
schema: 'schema.gql'
documents: 'src/graphql/**/*.graphql'
generates:
  src/generated/graphql.tsx:
    plugins:
      - 'typescript'
      - 'typescript-operations'
      - 'typescript-urql'
```

Run `yarn gen` in the root to generate your urql hooks.

Then, go to `App.tsx` and change the client endpoint:

```ts
// src/App.tsx
const client = createClient({
  url: '/.netlify/functions/github-graphql'
})
```

Note that we've removed both uses of `process.env.REACT_APP_GITHUB_TOKEN` from the frontend, which means our token is no longer exposed, making it more secure!

Finally, let's see if everything is working by changing `<PinnedRepos />` to only use the hello world hook. Here's what the whole file should look like (yes, delete all the old code, we'll put it back in later):

```tsx
// src/components/PinnedRepos.tsx
import React from 'react'
import { useHelloWorldQuery } from '../generated/graphql'

export const PinnedRepos: React.FC = () => {
  const [{ data }] = useHelloWorldQuery()
  return <>{data?.helloWorld ? data.helloWorld : null}</>
}

export default PinnedRepos
```

Now run `yarn build && netlify dev` to build and then serve the app. You should see 'hello world' on the page. Nice!

## Querying GitHub API

If you haven't already set up a GitHub token, head over to [Creating a personal access token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) and come back with your shiny new token.

We'll put that token in a `.env` file in root for testing:

```bash
# .env
GH_TOKEN=tokenString
```

Make sure to add `.env` to `.gitignore` if it's not already there!

Now, in the `functions` folder, open `github-graphql.ts` and let's create a functions that lets us query using `axios`. This is where that token is used for authorization:

```ts
// functions/src/functions/github-graphql/github-graphql.ts
import axios from 'axios'

const API_URL = 'https://api.github.com/graphql'
const headers = {
  authorization: `Bearer ${process.env.GH_TOKEN}`
}

const postAPI = async (query: string) =>
  await axios.post(API_URL, { query }, { headers })
```

If you didn't know, GraphQL isn't magic. A GraphQL query is just a POST request with a `query` object, so we're just going to keep it simple and use POST to do our backend queries.

We haven't created the query yet. Let's do that now in `queries.ts`:

```ts
// functions/src/functions/github-graphql/queries.ts

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

Now, we need to update our `schema.ts`:

```ts
// functions/src/functions/github-graphql/schema.ts

export default gql`
  type Repository {
    name: String
    description: String
    pushedAt: String
    url: String
    homepageUrl: String
  }

  type Query {
    helloWorld: String
    pinnedRepos: [Repository]
  }
`
```

Other than security, another nice thing about wrapping the API ourselves is we can restructure the data to make it cleaner to work with on the frontend.

So we'll be returning an array of a simplified `Repository` type rather than the deeply nested object that GitHub's API gives us. This will make more sense when you see the resolver.

Back to `github-graphql.ts`, we can update our resolvers and import the query:

```ts
// functions/src/functions/github-graphql/github-graphql.ts
import { PinnedReposQuery } from '/queries'

const resolvers = {
  Query: {
    helloWorld: async () => 'hello world',
    pinnedRepos: async () =>
      (await postAPI(PinnedReposQuery)).data.data.viewer.pinnedItems.edges.map((edge: any) => edge.node)
  }
}
```

So as you can see, we're grabbing the deeply nested data and returning a much simpler array. Let's finish up on the frontend.

## Displaying the Pinned Repos

First, we need to update our `schema.gql`:

```gql
type Repository {
  name: String
  description: String
  pushedAt: String
  url: String
  homepageUrl: String
}

type Query {
  helloWorld: String
  pinnedRepos: [Repository]
}
```

Now, we need to add the query file to the frontend. Add `PinnedRepos.graphql` to queries:

```graphql
// src/graphql/queries/PinnedRepos.graphql
query PinnedRepos {
  pinnedRepos {
    name
    description
    pushedAt
    url
    homepageUrl
  }
}
```

Run `yarn gen` to create the hook for urql, then head over to `PinnedRepos.tsx` to use that query instead of the hello world one and add some basic rendering from before:

```tsx
// src/components/PinnedRepos.tsx
import React from 'react'
import { usePinnedReposQuery } from '../generated/graphql'

export const PinnedRepos: React.FC = () => {
  const [{ data }] = usePinnedReposQuery()
  return (
    <>
      {data?.pinnedRepos ? (
        <div
          style={{
            display: 'flex',
            flexDirection: 'row',
            justifyContent: 'center',
            textAlign: 'left'
          }}
        >
          {data.pinnedRepos.map((repo, index) => {
            if (repo) {
              const { name, description, url, homepageUrl, pushedAt } = {
                name: '',
                description: '',
                url: '',
                homepageUrl: '',
                pushedAt: '',
                ...repo
              }
              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>
                  {url ? <a href={url}>View on GitHub</a> : null}
                  {homepageUrl ? (
                    <a href={homepageUrl} style={{ marginLeft: '1rem' }}>
                      View website
                    </a>
                  ) : null}
                </div>
              )
            } else {
              return null
            }
          })}
        </div>
      ) : (
        <p>Loading...</p>
      )}
    </>
  )
}

export default PinnedRepos
```

Now you can run `yarn build && netlify dev`. 

🎉 Everything should be working! If so, congrats, you did it! If not, feel free to drop questions in the comments (or compare to my deploy branch code).

## Deploying to Netlify

Next, we need to add the `GH_TOKEN` to your [deploy environment variables on Netlify](https://docs.netlify.com/configure-builds/environment-variables/#declare-variables):

> In your site dashboard under Settings > Build & deploy > Environment > Environment variables.

Once you add that on Netlify, and have everything working locally, you can push your changes to your repository's master branch on GitHub. Netlify will automatically build and deploy the new site.

## Resources

- [This Post's GitHub Repo](https://github.com/imjoshellis/GraphQL-Portfolio)
- [This Post's Demo on Netlify](https://graphql-portfolio.netlify.app)
- [How to Build an Automated Portfolio Using GitHub's GraphQL API and React](https://dev.to/imjoshellis/how-to-build-an-automated-portfolio-using-github-s-graphql-api-and-react-5bii)
- [Automate Your Portfolio with the GitHub GraphQL API](https://dev.to/imjoshellis/automate-your-portfolio-with-the-github-graphql-api-10e7)
- [urql docs](https://formidable.com/open-source/urql/docs/)
- [GitHub GraphQL API docs](docs.github.com/en/graphql/)
- [Test queries in your browser with the GitHub GraphQL explorer](https://developer.github.com/v4/explorer/)
- [My portfolio on GitHub](https://github.com/imjoshellis/imjoshellis.com)
- [My portfolio website](https://imjoshellis.com)

## 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!