Streamline your Vue 3 development with Fauna, Typescript, and GraphQL
Tags: typescript, graphql, fauna, vue
Published: 2021-04-27 23:24:26

*Written in connection with the [Write with Fauna Program](https://fauna.com/blog/write-with-fauna).*

Photo by <a href="https://unsplash.com/@srinii?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Srinivasan Venkataraman</a> on <a href="https://unsplash.com/s/photos/fauna?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>

GraphQL is amazing, but writing reducers and setting up the server side can be a bit daunting. Thankfully Fauna can do the heavy lifting and let us focus on the fun stuff!

To take it up a notch let’s sync up our GraphQL types with a code generator so that we can leverage Typescript and and VSCode’s intellisense for a seamless coding experience.

To demonstrate we will be building a simple orders list.

![order-list](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dcltacywc2hh1i7r8msv.png)

## Overview

1. Set up a vite app
2. Build a static version of order list
3. Build our schema and upload to Fauna
4. Use codegen to create types for our schema
5. Wire up the static list to live data
6. Bonus: Updating schema workflow

We’ll be using yarn, and vite for this project but you could just as easily use Vue CLI if you prefer and npm. Also if react is more your cup of coffee, most of this will still be applicable.

## Set up a vite app

Setting up vite is delightfully easy. Simply run `yarn create @vitejs/app` in your terminal and follow the prompts. I called mine “fauna-order-list” and chose the “vue-ts” template. Then cd into your directory and run `yarn`

That’s it!

Go ahead and run your app with `yarn dev`

Behold the mighty Vue starter template!

![vue-template](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c9x11v6623kct0oe63pf.png)

## Build a static version of order list

For quick styling we’ll be using tailwind. The tailwind documentation is great so [go here](https://tailwindcss.com/docs/guides/vue-3-vite#setting-up-tailwind-css) and follow their instructions.

Note: if you have the dev server running still you will need to restart the process to see tailwind’s styles.

Next create a new file `src/components/OrderList.vue` and paste in the following code:

```vue
<template>
  <div class="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
    <table class="min-w-full divide-y divide-gray-200">
      <thead class="bg-gray-50">
        <tr>
          <th scope="col" class="order-header">Name</th>
          <th scope="col" class="order-header">Address</th>
          <th scope="col" class="order-header">Order</th>
          <th scope="col" class="order-header">Total</th>
        </tr>
      </thead>
      <tbody>
        <tr class="bg-white">
          <td class="order-cell">
            <span class="bold">Jane Cooper</span>
          </td>
          <td class="order-cell">123 Main St, New York, NY 12345</td>
          <td class="order-cell">12oz Honduras x 1</td>
          <td class="order-cell">$12.00</td>
        </tr>
        <tr class="bg-gray-50">
          <td class="order-cell">
            <span class="bold">Bob Smith</span>
          </td>
          <td class="order-cell">456 Avenue G, New York, NY 12345</td>
          <td class="order-cell">12oz Honduras x 1, 12oz Ethiopia x 2</td>
          <td class="order-cell">$36.00</td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<style>
.order-cell {
  @apply px-6 py-4 whitespace-nowrap text-sm text-gray-500;
}

.order-header {
  @apply px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider;
}

.bold {
  @apply font-medium text-gray-900;
}
</style>
```

Now we need to add the `OrderList` component to our app.

Open up `src/App.vue` and replace all occurrences of `HelloWorld` with `OrderList`. While we’re in here let’s delete the `style` section and image as well. I’ve also wrapped `OrderList` in a section and added a couple styles to center and pad the list.

Your completed `App.vue` file should look like this.

```vue
<template>
  <section class="container mx-auto mt-8">
    <OrderList />
  </section>
</template>

<script lang="ts">
import { defineComponent } from "vue";
import OrderList from "./components/OrderList.vue";

export default defineComponent({
  name: "App",
  components: {
    OrderList,
  },
});
</script>
```

## Build our schema and upload to Fauna

### A basic schema

For our example data we will have two primary data objects. `Customers` and `Orders`.

`Customers` will have a `name` and `address`. `Orders` will have an array of `LineItems` and a reference to a `Customer`


Our basic schema looks like this:


```graphql
type Address {
  line1: String!
  line2: String
  city: String!
  state: String!
  postal_code: String!
}

type Customer {
  name: String!
  address: Address
}

type LineItem {
  name: String!
  amount: Int!
  quantity: Int!
}

type Order {
  items: [LineItem!]!
  customer: Customer!
}
```


Here we can see an `Address` type that could be used to store information like you might gather from a checkout form. Likewise, we have a `LineItem` type to hold individual items for a given order.

### Fauna specific directives

Fauna comes with [several directives](https://docs.fauna.com/fauna/current/api/graphql/directives/) to help auto generate resolvers for your schema. They’re powerful and well worth your time to read through the documentation. In this tutorial we will be covering two of them: `@collection` and `@embedded`.

In Fauna, all types in a graphql schema are considered `@collection` by default. If you are coming from a relational database background you can think of collections as being similar to tables.

I prefer to explicitly name my collections using the `@collection` directive to remove any ambiguity, and because I am finicky about my document names :grin:

The `@collection` directive takes one simple argument: “name” which is what Fauna will call the document for this collection.

Looking at our current schema we only want `customers` and `orders` collections. To prevent `Address` and `LineItem` from becoming documents we use the `@embedded` directive. This simply tells Fauna to not create separate collections for these types. These types are “embedded” directly in whatever other type references them.

Our final schema should now look like this:


```graphql
type Address @embedded {
  line1: String!
  line2: String
  city: String!
  state: String!
  postal_code: String!
}

type Customer @collection(name: "customers") {
  name: String!
  address: Address
  orders: [Order!] @relation
}

type LineItem @embedded {
  name: String!
  total: Int!
  quantity: Int!
}

type Order @collection(name: "orders") {
  items: [LineItem!]!
  customer: Customer!
}
```


Save this to `src/graphql/schema.graphql`

### Upload to Fauna

Navigate to https://dashboard.fauna.com and create a new database. I called my “fauna-order-list”, the same as my vite app. Then select “GraphQL'' from the left sidebar.

Click the import button and select `schema.graphql`

After a few moments you should have the GraphQL Playground open.

### Add some data

The GraphQL Playground is actually pretty handy for creating test data. We won’t cover updating information from the app in this tutorial, but generally when writing queries I write and execute them in the GraphQL Playground first. Then copy and paste those queries as string constants locally. This way I know that my queries are correct.

### Create a customer

Since our orders need a customer to reference, let’s start by creating a customer.

Fauna has already generated create mutations for both orders and customers. You can see this in the very helpful auto-completions in the playground. If you have never used a GraphQL playground before you can see what is available at any time by opening up the “DOCS” tab on the right hand side of the window.

Your query should look like this at this point:


```graphql
mutation {
  createCustomer
}
```


Note how there is a red indicator to the left of line 2. If you hover over “createCustomer” you will get a helpful popover with error information. In this case it is asking for subfields to return, and saying that a “CustomerInput!” is required.

For our purposes we just need the "_id". And for the CustomerInput we’re going to use a variable, so we add a variable reference as well.

Our final mutation looks like this, and our error messages have all been resolved:


```
mutation ($data: CustomerInput!) {
  createCustomer(data: $data) {
    _id
  }
}
```


Now to provide a value for `$data` at the bottom of the tab you will find “QUERY VARIABLES” and “HTTP HEADERS”

Click “QUERY VARIABLES”. You can also drag the variables tab up to create more room.

Inside the variables tab we have the same auto-complete functionality as above. You can `ctrl+space` to see what options are available, or just start typing if you already know what you need.

Here you will see. that since `createCustomer` needs a `data` parameter, that is what auto-complete will recommend. Also notice as you add properties, the error hints that show up. These will all guide you toward providing correctly formatted data as per your schema.

Once you have your variable filled out, click the play button to see your result. Since we only asked for `_id` to be returned that is what you should see in the result tab.

Here is my final variable for reference:


```graphql
{
  "data": {
    "name": "John Smith",
    "address": {
      "line1": "123 Main St",
      "city": "Austin",
      "state": "TX",
      "postal_code": "12345"
    }
  }
}
```

### Create an order

Since orders need a customer reference we’re going to leave this tab open and start a new tab for the orders. Click the `+` tab right next to the `createCustomer` tab. As a quick aside, notice how the tabs are helpfully named after the query that is being run and that the type of query is labeled as well. In this case, “M” for mutation.

The `createOrder` mutation follows the same process as before. The interesting part here is how we connect customers to orders. Take a look at the final variable here:


```
{
  "data": {
    "customer": {
      "connect": "294049263884698114"
    },
    "items": [
      {
        "name": "12oz Honduras",
        "total": 1200,
        "quantity": 1
      }
    ]
  }
}
```


That `connect` parameter is what “links” an order to a customer. In the auto-complete when writing this variable you may have noticed another option, `create`.

This is one of the really cool and powerful things about GraphQL. This kind of flexibility allows us to create a customer and an order at the same time, while simultaneously linking them together. And Fauna took care of all the resolvers for us, so we get all of this functionality for free! :heart_eyes:

Here is an example variable parameter to use with `createOrder` that does all of the above:


```graphql
{
  "data": {
    "items": [
      {
        "name": "12oz Ethiopia",
        "quantity": 1,
        "total": 1200
      }
    ],
    "customer": {
      "create": {
        "name": "Jane Doe",
        "address": {
          "line1": "456 Ave A",
          "city": "New York",
          "state": "NY",
          "postal_code": "11111"
        }
      }
    }
  }
}
```

### Listing the orders

Now that we have some test data let’s open another GraphQL Playground tab and write the select query. Using `ctrl+space` you should notice something missing. Currently it looks like we can only “findByCustomerID'' and “findByOrderID” :thinking:

At the time of this writing we need to explicitly create an `@index` to display our Orders.

In plain GraphQL we are simply defining a `Query`. Since we want to return “all orders” let’s call this one `allOrders`. This will be an array of `Order`s and we want to insist that it return something so our basic GraphQL query looks like this:


```graphql
type Query {
  allOrders: [Order!]!
}
```


And to define the index we simply add an `@index` directive with a name so our final query looks like this:


```graphql
type Query {
  allOrders: [Order!]! @index(name: "all_orders")
}
```


We add this to our `schema.graphql` file, then back in GraphQL Playground select “UPDATE SCHEMA” from the top of the screen and upload our updated schema.

After a brief moment the schema is updated and we have our `allOrders` query available like so:


```graphql
{
  allOrders {
    data {
      _id
      customer {
        name
        address {
          line1
          line2
          city
          state
          postal_code
        }
      }
      items {
        name
        total
        quantity
      }
    }
  }
}
```


Execute this query in the playground and we see our two orders! Awesome.

Notice the nested `data` object. This is because there are three top level properties to the return object when querying GraphQL. `data` holds the data for the current result set. `before` and `after` hold cursors for paginating the results. We will not go over pagination in this tutorial, but it’s good to remember that when extracting this information later that you need to reference `data` since the array is not available at the top level.

Now that we have a working query let’s save it locally for use later in our app. Save this query to `src/graphql/queries.ts` like this:


```typescript
export const allOrders = `
{
  allOrders {
    data {
      _id
      customer {
        name
        address {
          line1
          line2
          city
          state
          postal_code
        }
      }
      items {
        name
        total
        quantity
      }
    }
  }
}`;
```

## Use codegen to create types for our schema

Now that our data is ready to go, let's set up codegen to generate types for us to use in our app.

For detailed instructions follow [the documentation here](https://www.graphql-code-generator.com/docs/getting-started/installation). The quick version is listed below.

Execute the following commands:

```bash
yarn add graphql
yarn add -D @graphql-codegen/cli @graphql-codegen/typescript
yarn graphql-codegen init
```

Here are the responses I used for the prompts:

![prompts](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5wqirxvs8fowush2y19z.png) 

Now if you try running `yarn codegen` you will get an error. :scream:

We need to provide an authorization header to connect to Fauna.

### Add authorization header

1. Go to the Fauna dashboard for this database and select “Security” from the left side menu
2. Click the “NEW KEY” button
3. Choose “Server” for the role and save
4. Copy the key’s secret to your clipboard
5. Create a `.env` file in you app’s root directory
6. Save the key’s secret to `.env` like so: `VITE_FAUNA_KEY=PASTE_HERE`
7. In `package.json` update the `codegen` script by adding “-r dotenv/config” so that it looks like this: `"codegen": "graphql-codegen --config codegen.yml -r dotenv/config"`
8. Open `codegen.yml` and add the Authorization header, and remove the “documents” key like so:

   1. `overwrite: true`

      ```yaml
      schema:
        - https://graphql.fauna.com/graphql:
            headers:
              Authorization: Bearer ${VITE_FAUNA_KEY}
      generates:
        src/schema.d.ts:
          plugins:
            - "typescript"
      
      ```


Try running `yarn codegen` again. Hooray :tada:

Go take a look at `schema.d.ts` and see all the typing you just saved. It’s not important to understand every line that was created here. Just that these types will help ensure that we correctly format our GraphQL queries correctly, and that our data structures locally are in sync with the server.

### Some notes on security and best practices

We are using Vites’ built in `dotenv` functionality to easily store our secret key locally, but in a real application you should never do this. The reason is that when we use the secret key later in our app Vite will actually include it in the bundled code which means anyone picking through your source could see it.

In a real application this would be stored in an environment variable on a server and you would access that api instead of hitting Fauna directly. This is beyond the scope of this tutorial, just know that you should never include secret keys in a repository, or expose them publicly.

## Wire up the static list to live data

Finally we get to pull it all together.

### Use Suspense to display async components

First open up `App.vue` and wrap `OrderList` in a `Suspense` tag to prepare for rendering components with async data

```vue
<Suspense>
  <OrderList />
</Suspense>
```

### Use fetch to retrieve the orders

For simplicity we’re going to just use `fetch` to natively retrieve the orders.

Open `OrderList.vue` and add a script tag using typescript and the `setup` option `&lt;script lang="ts" setup>` Note that `setup` is still in RFC so it _may_ not make it into the final version of Vue 3. But it has a ton of support from the community and is most likely stable at this point. I believe it greatly reduces unnecessary boilerplate and makes for a better development experience so I’m using it in these examples.

Also note that we’ll be using Vue 3’s composition API.

Thinking through our data, the first thing we’ll need is a `ref` to an `array` to hold the orders. Then we’ll await a `fetch` post to Fauna and store the data in `orders.value`

```typescript
import { ref } from "vue";
import { allOrders } from "../graphql/queries";
import type { Order } from "../schema";

const orders = ref<Order[]>([]);
orders.value = await fetch("https://graphql.fauna.com/graphql", {
  method: "POST",
  body: JSON.stringify({ query: allOrders }),
  headers: {
    Authorization: `Bearer ${import.meta.env.VITE_FAUNA_KEY}`,
  },
})
  .then((res) => res.json())
  .then(({ data }) => data.allOrders.data);
```

Run the dev server `yarn dev` and open up the app in your browser. Using [vue-devtools](https://github.com/vuejs/vue-devtools) inspect the `OrderList` component and see that it now has an `orders` property under `setup` populated with our data. Neat!

### Prepare the data for easier display

We’ll use a computed value to map the `orders` array into an easier to consume format. For example, `customer.address` is currently an object as you might expect it to be stored. But we just want a string to display in the table, so we will use `map` to create a new `parsedOrders` array formatted how we want it.

```typescript
const parsedOrders = computed(() => {
  return orders.value.map((o) => ({
    _id: o._id,
    name: o.customer.name,
    address: `${o.customer.address?.line1} ${o.customer.address?.city}, ${o.customer.address?.state} ${o.customer.address?.postal_code}`,
    items: o.items.map((i) => `${i.name} x ${i.quantity}`).join(", "),
    total: new Intl.NumberFormat("en-US", {
      style: "currency",
      currency: "USD",
    }).format(o.items.reduce((pre, cur) => pre + cur.total, 0) / 100),
  }));
});
```

Thanks to our generated schema types our IDE gives us helpful autocomplete for our deeply nested objects such as `o.customer.address?.line1` and even knows to safely check that nullable fields are available.

Now we just swap out our static HTML for our `parsedOrders` data:

```vue
<tr
    v-for="(order, index) in parsedOrders"
    :key="order._id"
    :class="{ 'bg-gray-50': index % 2 !== 0 }"
    class="bg-white"
    >
  <td class="order-cell">
    <span class="bold">{{ order.name }}</span>
  </td>
  <td class="order-cell">{{ order.address }}</td>
  <td class="order-cell">{{ order.items }}</td>
  <td class="order-cell">{{ order.total }}</td>
</tr>
```

Our final `OrderList` component should look like this:

```vue
<template>
  <div class="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
    <table class="min-w-full divide-y divide-gray-200">
      <thead class="bg-gray-50">
        <tr>
          <th scope="col" class="order-header">Name</th>
          <th scope="col" class="order-header">Address</th>
          <th scope="col" class="order-header">Order</th>
          <th scope="col" class="order-header">Total</th>
        </tr>
      </thead>
      <tbody>
        <tr
          v-for="(order, index) in parsedOrders"
          :key="order._id"
          :class="{ 'bg-gray-50': index % 2 !== 0 }"
          class="bg-white"
        >
          <td class="order-cell">
            <span class="bold">{{ order.name }}</span>
          </td>
          <td class="order-cell">{{ order.address }}</td>
          <td class="order-cell">{{ order.items }}</td>
          <td class="order-cell">{{ order.total }}</td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<script lang="ts" setup>
import { ref, computed } from "vue";
import { allOrders } from "../graphql/queries";
import type { Order } from "../schema";

const orders = ref<Order[]>([]);
orders.value = await fetch("https://graphql.fauna.com/graphql", {
  method: "POST",
  body: JSON.stringify({ query: allOrders }),
  headers: {
    Authorization: `Bearer ${import.meta.env.VITE_FAUNA_KEY}`,
  },
})
  .then((res) => res.json())
  .then(({ data }) => data.allOrders.data);

const parsedOrders = computed(() => {
  return orders.value.map((o) => ({
    _id: o._id,
    name: o.customer.name,
    address: `${o.customer.address?.line1} ${o.customer.address?.city}, ${o.customer.address?.state} ${o.customer.address?.postal_code}`,
    items: o.items.map((i) => `${i.name} x ${i.quantity}`).join(", "),
    total: new Intl.NumberFormat("en-US", {
      style: "currency",
      currency: "USD",
    }).format(o.items.reduce((pre, cur) => pre + cur.total, 0) / 100),
  }));
});
</script>

<style>
.order-cell {
  @apply px-6 py-4 whitespace-nowrap text-sm text-gray-500;
}

.order-header {
  @apply px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider;
}

.bold {
  @apply font-medium text-gray-900;
}
</style>
```

## Bonus: Updating schema workflow

Most of our schema’s evolve over time as new requirements come in. Let’s walk through a very simple schema update and how we would update our app to accommodate.

In this example let’s say we now want to collect email addresses. First we add `email` to our `schema.graphql`

```graphql
type Customer @collection(name: "customers") {
  name: String!
  address: Address
  email: String
  orders: [Order!] @relation
}
```

Note: for simplicity’s sake I am leaving email as not required. If we made email required like this `email: String!` then we would also have to write a data migration script to update all the existing documents, because our `allOrders` query would now fail.

Now that our schema is updated locally, we update the schema in Fauna’s dashboard just as we did before by going to the GraphQL tab, clicking “UPDATE SCHEMA” and uploading our updated schema file.

While we’re in the dashboard let’s go ahead and update a customer to have an email address. I found this easiest to do by going to the Collections tab and editing a document directly. Pick your first customer and add an email field.

Go back to the GraphQL tab and select the `allOrders` tab. Add email to your query under customer and you should now see emails being returned.

Copy and paste this query back into `queries.ts` and now we should be ready to display emails in our app.

Run `yarn codegen` to sync up with Fauna

Open up `OrderList.vue` and add email to the `parsedOrders` computed variable.

Then simply display `order.email` next to the customer’s name. Note, you might have to refresh the page to get the schema updates to take effect.

That’s it!

## Conclusion

I’ve been really enjoying using Fauna’s GraphQL endpoint. It has greatly streamlined my process for development. Considering it is still in early stages it’s only going to get better from here, which is pretty incredible.