Simple GraphQL with Fauna
Tags: graphql, fauna, typescript, nextjs
Published: 2020-10-29 14:52:41

How I am using the [fauna-gql-upload](https://github.com/Plazide/fauna-gql-upload) package, authored by [Carl Hallén Jansson](https://twitter.com/chj_web) to simplify my Fauna GraphQL development workflow.

## What I love about Fauna

At first glance, there is a lot to like about [Fauna](https://dashboard.fauna.com/accounts/register?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_SimpleGraphQLFaunaDB_SConnolly). First, **it is a managed database**. As a frontend engineer, I find the persnickety details and behaviors of databases cumbersome. I jump at any chance to avoid managing servers, dealing with state, and setting up my local development with a heavy database. Fauna is a backend as a service, so you don't need to worry about any of that. You setup your database in their UI, get your private key to access it and away you go. I love that experience.

I also love that **Fauna has a GraphQL layer**. I upload my schema and Fauna generates the basic queries, mutations and underlying implementation details like resolvers for me so I can start executing GraphQL queries immediately.

This is all great, but we can improve the Fauna GraphQL developer experience even further with some additional automation.

## Why we need to automate

If you follow Fauna's [Get started with GraphQL](https://docs.fauna.com/fauna/current/start/graphql) tutorial, you will need to use Fauna's admin UI to create and modify your GraphQL schema. That is fine to get started, but we need something more automated so we can continue to enhance our schema as our application evolves.

We don't want to upload a `schema.gql` file in Fauna's admin UI every time we need to modify our GraphQL server. What we need is:

- The ability to quickly and easily push changes to our GraphQL server beyond just the schema.
- The ability to know what other changes went along with our Fauna changes.
- The knowledge of who made those changes and why.
- The ability to revert to a prior state.

One way of doing this is to use [fauna-gql-upload](https://github.com/Plazide/fauna-gql-upload), a simple CLI to update your database's GraphQL schema, resolver functions, indexes, and database roles without going to the Fauna dashboard.

Yes, that's more like it! I can manage my GraphQL server as code just like any other code that powers my application. I can open PRs with the proposed changes and have my team review them before I commit them to my production database.

## What we are building

In this tutorial, we are going to build a basic game of Rock Paper Scissors. By the end, you will be able to play a game with your Fauna backend using just GraphQL queries and mutations.

## Setup your database

Before we can start using [fauna-gql-upload](https://github.com/Plazide/fauna-gql-upload), we need to set up a database. You can follow the [Quick start with Fauna](https://docs.fauna.com/fauna/current/start/cloud) or use the admin UI to create your database. I will walk you through how to do this in the UI. After this you won’t need to manage anything else in Fauna’s UI, though it is still helpful to use it for document inspection, running one-off commands in the shell and executing GraphQL queries.

1. From the Fauna dashboard, select `New Database`.
![New database](https://dev-to-uploads.s3.amazonaws.com/i/qmylox4rxb8mlrb22c5t.png)

2. For the Database Name, enter `rock-paper-scissors`.
![Enter database name](https://dev-to-uploads.s3.amazonaws.com/i/f1ohgdywbejsicn7rdqu.png)

3. In the left nav, select `Security` and then select `New Key`.
![Security new key](https://dev-to-uploads.s3.amazonaws.com/i/fg5ydt5x7eqf6r1oovs6.png)

4. For the `Role`, make sure it is `Admin` and then enter `fauna-gql` as the `Key Name`, then select `Save`.
![Security select role](https://dev-to-uploads.s3.amazonaws.com/i/t6klxmmosf7e55o4sev3.png)

5. Keep the window open or store the secret key somewhere safe. We will use it in a moment.

## Setup your project

Before we do anything with Fauna, we will set up our code project. To simplify setup for you, I created a simple generator CLI built on top of [hygen.io](https://hygen.io). You can view the template files for that [in this GitHub repository](https://github.com/goldenshun/shun/tree/master/_templates/fauna/gql).

1. [Create a GitHub repository](https://github.com/new).
![Create GitHub repo](https://dev-to-uploads.s3.amazonaws.com/i/mkcx7pbv5vfp5ufdt8fn.png)

2. Clone that repository to a local directory, i.e. `git clone <repository url>`.

3. In the created directory, run `npx @goldenshun/shun fauna gql`.

4. Follow the prompts to enter the name (rock-paper-scissors) and description for your project.

5. When prompted for your Fauna server key, enter the key you generated in step 5 of the `Setup your database` step.
![CLI enter key](https://dev-to-uploads.s3.amazonaws.com/i/15wcxko92p01k2td35wx.png)

You should now have several configuration files in your project as well as a `fauna` directory. This also installed several dependencies, which is all you need to leverage [fauna-gql-upload](https://github.com/Plazide/fauna-gql-upload) to create your GraphQL server. Let's take a look at a few key files.

`fauna/schema.gql`

This declares a simple `Example` model with property `foo`, which is a string.

```graphql
type Example {
  foo: String!
}
```

`package.json`

There are a few `scripts` defined here, but the main one we will be using is `fauna`, which will compile our TypeScript files and then upload our GraphQL schema plus supporting FQL files via [fauna-gql-upload](https://github.com/Plazide/fauna-gql-upload).

```json
"scripts": {
  "build:fauna": "tsc --project tsconfig.fauna.json",
  "fauna": "npm run build:fauna && fauna-gql",
  "fauna:override": "npm run build:fauna && fauna-gql --override"
}
```

> Is TypeScript required?

TypeScript is not required to use [fauna-gql-upload](https://github.com/Plazide/fauna-gql-upload). The default examples for that project are all in vanilla JavaScript. I prefer TypeScript for my projects because it makes it easier for me to find small mistakes like typos and misplaced arguments when I am writing complex FQL statements. Use whatever works for you though!

`.fauna.json`

This file provides configuration to [fauna-gql-upload](https://github.com/Plazide/fauna-gql-upload) so it knows where to find your schema, supporting FQL files and the secret you entered in the `Setup your project` step.

```json
{
  "envPath": ".env.local",
  "secretEnv": "FAUNA_ADMIN_SECRET_KEY",
  "schemaPath": "fauna/schema.gql",
  "fnsDir": ".gen/fauna/functions",
  "rolesDir": ".gen/fauna/roles",
  "indexesDir": ".gen/fauna/indexes"
}
```

## Create your GraphQL server

Now that you have your Fauna database and your code project setup, you are ready to run it!

`npm run fauna`

You should see this output in your terminal:

```bash
✔️  Successfully updated schema.

✔️  Successfully uploaded indexes

✔️  Successfully uploaded functions

✔️  Successfully uploaded roles
```

As you can see, in one command we pushed our GraphQL type definitions as well as supporting indexes, functions, and roles.

## Test your GraphQL server

1. Back in the Fauna Admin UI, select `GraphQL` in the left nav.
![GraphQL create](https://dev-to-uploads.s3.amazonaws.com/i/oegw5lk2btle3ialdab8.png)

2. Open the `Docs` tab. You should see the `findExampleByID` query as well as several mutations, including `createExample`.

🎉 This is what Fauna does _for_ you. Remember, all you uploaded in your `schema.gql` file was a model called `Example`. But after uploading your schema to Fauna, you now have the ability to create, read, update, and delete (CRUD) documents.

3. Enter the following GraphQL mutation in the GraphQL playground and execute it.

```graphql
mutation {
  createExample(data: { foo: "Hello" }) {
    _id
  }
}
```

If all went well, you should see a result like this:

```graphql
{
  "data": {
    "createExample": {
      "_id": "278019502149468680"
    }
  }
}
```

## Customizing your GraphQL server

What we've done so far is pretty cool. In just a few CLI commands, we were able to bootstrap a fully functional GraphQL server. Now that we know everything is up and running, let's implement something more interesting.

In order to play a game of Rock Paper Scissors with our Fauna backend, we need some additional functionality.

The basic game flow should look like this:

1. You, the player, make a selection.
2. The opponent (programmed in Fauna) also makes a selection.
3. The winner is determined based on the rules of the game.
4. The results are sent back to you, the player.

Simple enough, let's implement it!

### Initial schema

First, we can define an `enum` that declares all possible selections in the `fauna/schema.gql` file.

```graphql
enum Selection {
  Rock
  Paper
  Scissors
}
```

Next, we define what the results should look like. We will use a message like `You win!` or `You lose!` to display the results.

```graphql
type Result @embedded {
  selection: Selection!
  opponentSelection: Selection!
  message: String!
}
```

Last, we need a way for you, the player, to make your selection. In this case, the automatic CRUD queries and mutations generated by Fauna are not enough. In order to facilitate the additional logic of our game, **we need a custom resolver**.

> What is that `@embedded` directive?

By labeling the `Result` type as embedded, it means Fauna will not create an associated collection nor additional queries and mutations associated with it. This is good for our purposes because we are merely defining the output shape of our game results, we are not planning to store those results anywhere.

For more information, refer to Fauna's docs on the [embedded directive](https://docs.fauna.com/fauna/current/api/graphql/directives/d_embedded).

### Implement a custom resolver

To implement a custom resolver, you need 2 things:

1. A [user-defined function](https://docs.fauna.com/fauna/current/api/graphql/functions) (UDF).
2. A query or mutation that references your UDF with the [resolver](https://docs.fauna.com/fauna/current/api/graphql/directives/d_resolver) directive.

This is where [fauna-gql-upload](https://github.com/Plazide/fauna-gql-upload) really shines.

1. Create a new file `fauna/lib/DetermineResult.ts` and add the contents below.

This is a helper function that we'll use to compare our selection with a randomized opponent selection and determine a winner. Since it lives under `/lib` and not `/functions`, it will not be registered as a UDF in Fauna.

```TypeScript
import { ExprArg, ExprVal, query } from "faunadb";
const { And, Equals, If, Or } = query;

export default function DetermineResult(selection: ExprArg, opponentSelection: ExprArg): ExprVal {
  return {
    selection,
    opponentSelection,
    message: If(
      Equals(selection, opponentSelection),
      "It's a tie!",
      If(
        Or(
          And(
            Equals(selection, "Rock"),
            Equals(opponentSelection, "Scissors")
          ),
          And(
            Equals(selection, "Paper"),
            Equals(opponentSelection, "Rock")
          ),
          And(
            Equals(selection, "Scissors"),
            Equals(opponentSelection, "Paper")
          ),
        ),
        "You win!",
        "You lose!"
      )
    )
  }
}
```

2. Now create the file `fauna/functions/Play.ts` and add the contents below.

This is our UDF. Note that it references the `DetermineResult` helper function we created above. Since FQL is somewhat verbose, I like to organize my functions like this to keep the logic readable and clean.

This UDF also makes use of some additional helper functions from [fauna-fql-lib](https://github.com/shiftx/faunadb-fql-lib). Fauna does not provide its own random number generator (yet), but the `RandomString` function is good enough for our purposes.

The `name` property is especially important as it will serve as the identifier in the `@resolver` directive in `schema.gql`.

```TypeScript
import { query } from "faunadb";
import { RandomString, Switch } from "faunadb-fql-lib";
import DetermineResult from '../lib/DetermineResult';
const { Lambda, Let, Query, Var } = query;

const selectionMap = {
  R: "Rock",
  P: "Paper",
  S: "Scissors"
};

const Play = {
  name: "play",
  body: Query(
    Lambda(
      ["selection"],
      Let({
        randomCharacter: RandomString(1, "RPS"),
        opponentSelection: Switch(Var("randomCharacter"), selectionMap),
      },
        DetermineResult(Var("selection"), Var("opponentSelection"))
      )
    )
  ),
};

export = Play;
```

3. With the UDF defined, we can now reference it in our GraphQL schema.

Open `fauna/schema.gql` again and add the following query.

```graphql
type Query {
  play(selection: Selection!): Result! @resolver(name: "play")
}
```

This defines a query called `play` that takes one argument, `selection`, which will be one of the `enum` values defined earlier (Rock, Paper, or Scissors). The query returns a non-nullable `Result` and the `@resolver` directive tells Fauna to use the `play` UDF we defined in the `fauna/functions/Play.ts` file we implemented earlier.

### Make it so

So far, we've accomplished the 2 aformentioned requirements for a custom resolver all in our code editor:

1. We updated our schema in `schema.gql`
2. We defined a UDF to implement our game logic.

Now, in one command we can push all of this to Fauna:

`npm run fauna`

Again, you should see the following output:

```bash
✔️  Successfully updated schema.

✔️  Successfully uploaded indexes

✔️  Successfully uploaded functions

✔️  Successfully uploaded roles
```

## Let's play!

Head on over to the Fauna admin UI again. Go to the GraphQL section and execute the query below.

```graphql
query {
  play(selection: Rock) {
    selection
    opponentSelection
    message
  }
}
```

You should see output like the following:

```graphql
{
  "data": {
    "play": {
      "selection": "Rock",
      "opponentSelection": "Scissors",
      "message": "You win!"
    }
  }
}
```

## Conclusion

In this tutorial, we laid a strong foundation for developing any application with Fauna. We can make changes in our code editor and push them to Fauna in a single command. The full source code for this project can be found at [fauna-rock-paper-scissors](https://github.com/goldenshun/fauna-rock-paper-scissors).

If you want to experience a more complete example of what you can build with Fauna and GraphQL, head on over to [Botwise Games](https://botwise.games/). I was grateful to find [fauna-gql-upload](https://github.com/Plazide/fauna-gql-upload) to make my Fauna developer experience for that project even better. Special thanks again to [Carl Hallén Jansson](https://twitter.com/chj_web) for authoring this library and continuing to improve it.

If you have any questions or comments on this tutorial, find me on Twitter [@seanconnollydev](https://twitter.com/seanconnollydev) or [seanconnolly.dev](https://seanconnolly.dev).

## Up next

In the next post in this series, we will learn how to use Fauna's generated GraphQL schema to build UI for our game with Next.js, TypeScript and Apollo hooks. See you then!