Written in connection with the Write with Fauna Program.
Photo by Srinivasan Venkataraman on Unsplash
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.
Overview
- Set up a vite app
- Build a static version of order list
- Build our schema and upload to Fauna
- Use codegen to create types for our schema
- Wire up the static list to live data
- 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!
Build a static version of order list
For quick styling we’ll be using tailwind. The tailwind documentation is great so go here 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:
<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.
<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:
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 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 😁
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:
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:
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:
{
"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! 😍
Here is an example variable parameter to use with createOrder
that does all of the above:
{
"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” 🤔
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:
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:
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:
{
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:
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. The quick version is listed below.
Execute the following commands:
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:
Now if you try running yarn codegen
you will get an error. 😱
We need to provide an authorization header to connect to Fauna.
Add authorization header
- Go to the Fauna dashboard for this database and select “Security” from the left side menu
- Click the “NEW KEY” button
- Choose “Server” for the role and save
- Copy the key’s secret to your clipboard
- Create a
.env
file in you app’s root directory - Save the key’s secret to
.env
like so:VITE_FAUNA_KEY=PASTE_HERE
- In
package.json
update thecodegen
script by adding “-r dotenv/config” so that it looks like this:"codegen": "graphql-codegen --config codegen.yml -r dotenv/config"
-
Open
codegen.yml
and add the Authorization header, and remove the “documents” key like so:-
overwrite: true
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 🎉
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
<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 <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
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 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.
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:
<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:
<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
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.
Top comments (0)