Build and Deploy a GraphQL API to the Edge with Fauna

Build and Deploy a GraphQL API to the Edge with Fauna

In this guide we'll build a GraphQL API with Grafbase, and persist data to Fauna using their SDK.

Fauna is a distributed relational database with a document data model delivered as a cloud API. It offers advanced querying capabilities, robust consistency guarantees, and comprehensive support for data operations. These attributes make Fauna an ideal fit for secure multi-tenant apps, distributed real-time apps, user-centric apps, or stateful serverless apps.

Developers can leverage its strongly typed database language, FQL v10, to express complex business logic in transactions. With multi-region, active-active compute and a distributed transaction engine, Fauna ensures fast, reliable, and secure performance.

Grafbase simplifies the process of creating and implementing your personalized GraphQL API at the edge while ensuring complete end-to-end type safety.

By using resolvers and connectors, Grafbase allows seamless integration with any data source. Additionally, you can take advantage of features such as edge caching, authentication and permission rules configuration, serverless search functionality, and more.

Grafbase's versatility extends to local development as well, thanks to the Grafbase CLI. Each Git branch has its preview deployment, facilitating convenient testing and collaboration.

You should already have a Fauna account to follow along.

Begin by creating a new Grafbase project. If you have an existing frontend that you want to create a GraphQL API, you must run the following command inside that project directory:

npx grafbase init graphql-fauna-api

This command will ask if you want to create a project using GraphQL SDL or TypeScript.

Fauna is perfect for creating databases and databases inside of databases where you require multi-tenant support for building and scaling applications.

We'll be using a single database in this guide as well as the UI for creating a database and collection but everything you see below can be done using Fauna's FQL syntax.

You'll need to sign up for a Fauna account, create a database, a products collection, and obtain an API key.

Fauna is perfect for creating databases and databases inside of databases where you require multi-tenant support for building and scaling applications.

We'll be using a single database in this guide as well as the UI for creating a database and collection but everything you see below can be done using Fauna's FQL syntax.

You'll need to sign up for a Fauna account, create a database, a products collection, and obtain an API key.

  1. Sign up for a Fauna account

  2. Create a new database

Create Database

  1. Create a product collection

Create collection

  1. Create a database key (Security > Database Keys > New Key)

Create the database key

  1. Copy the key

Copy the database key

  1. Add the key to .env
FAUNA_SECRET=

Now we will install the Fauna JavaScript driver inside our project so that we can use it inside our first resolver:

npm install fauna graphql

Now create the file resolvers/create.ts.

We'll complete this inside the next step but for now, we can import Fauna and initialize a new client:

import { Client, FaunaError, fql } from 'fauna' const client = new Client()

Fauna automatically detects the environment variable FAUNA_SECRET. If you name it something else, you'll need to pass it to Client().

We're now ready to create a GraphQL mutation that we will use to add to our products collection inside the Fauna database.

A GraphQL mutation is a type of operation in GraphQL that modifies data on the server and typically look something like this:

mutation { doSomething(a: String, b: Int) { someField } }
  • doSomething is the name of the mutation
  • a and b are names of the arguments passed to mutations
  • String and Int are the data types of the arguments
  • someField is the name of a field returned by the mutation

If you selected TypeScript as the configuration type when using grafbase init you will have the file grafbase.config.ts. Inside here we will add the type for Product with the following fields:

  • id
  • name
  • price
import { config, graph } from '@grafbase/sdk' const g = graph.Standalone() const product = g.type('Product', { id: g.id(), name: g.string(), price: g.int(), }) export default config({ graph: g, })

Now we have the Product type created, we can now use that for the createProduct mutation. We'll need to create a input type used by the mutation and configure the mutation itself, which links the resolver file create.ts`:

const productCreateInput = g.input('ProductCreateInput', { name: g.string(), price: g.int(), }) g.mutation('productCreate', { args: { input: g.inputRef(productCreateInput) }, resolver: 'products/create', returns: g.ref(product).optional(), })

Now let's create the code that runs when the GraphQL mutation productCreate is executed. This code is known as a GraphQL resolver.

In the previous step, we created the file resolvers/create.ts. This is the file we will use to export a default async function that invokes a Fauna FQL statement to insert into the database.

Let's update the file create.ts to contain a new default export:

import { Client, FaunaError, fql } from 'fauna' const client = new Client() export default async function ProductsCreate(_, { input }) { // ... }

Since Fauna is a document based database, we don't need to give it a structure upfront like you would with MySQL or Postgres.

Since we have the collection products we can invoke products.create() and pass it the input type and return the id field (managed by Fauna) and the name/price fields:

export default async function ProductsCreate(_, { input }) { try { const documentQuery = fql` products.create(${input}) { id, name, price } ` const { data } = await client.query(documentQuery) return data } catch (error) { if (error instanceof FaunaError) { console.log(error) } return null } }

That's all we need to successfully create a product inside the Fauna products collection using a GraphQL mutation. There's minimal error handling, you'll want to add to that.

Now run the Grafbase CLI:

npx grafbase dev

Next open Pathfinder at http://127.0.0.1:4000 and execute the following mutation:

mutation { productCreate(input: { name: "Shoes", price: 1000 }) { id name price } }

You should get a response back that looks something like this:

{ "data": { "productCreate": { "id": "372390645805875406", "name": "Shoes", "price": 1000 } } }

That's it! You can repeat this mutation as many times as you like with unique content!

Let's now move on to creating a GraphQL mutation to update products by ID.

Let's begin by updating the Grafbase Configuration to add a mutation that accepts the following arguments:

  • by — something we can use to pass id to target the product we want to update
  • input — the actual input of the fields we want to update. These fields should be optional.

Inside grafbase.config.ts you will want to add the following:

const productUpdateInput = g.input('ProductUpdateInput', { name: g.string().optional(), price: g.int().optional(), }) const productByInput = g.input('ProductByInput', { id: g.id().optional(), }) g.mutation('productUpdate', { args: { by: g.inputRef(productByInput), input: g.inputRef(productUpdateInput), }, resolver: 'products/update', returns: g.ref(product).optional(), })

Next, create the file resolvers/products/update.ts and add the following:

import { Client, FaunaError, fql } from 'fauna' const client = new Client() export default async function ProductsUpdate(_, { by, input }) { const { id } = by try { const documentQuery = fql` products.byId(${id}).update(${input}) { id, name, price } ` const { data } = await client.query(documentQuery) return data } catch (error) { if (error instanceof FaunaError) { console.log(error) } return null } }

FQL is amazing! We can update by ID and return the data in the same request... It feels very much like GraphQL! 🤩

Let's finish by adding some additional validation to the resolver. We want to check at least one by and input field is present. Let's update the ProductsUpdate resolver to include the following:

import { GraphQLError } from 'graphql' export default async function ProductsUpdate(_, { by, input }) { const { id } = by if (Object.entries(input).length === 0) { throw new GraphQLError('At least one field to update must be provided.') } // ... }

You can now execute the GraphQL mutation updateProduct and pass it on of the id values that were returned from the previous createProduct mutations.

Open Pathfinder at http://127.0.0.1:4000 and execute the following:

mutation { productUpdate( by: { id: "372390645805875406" } input: { name: "New shoes" } ) { id name price } }

You should get a response that looks something like this:

{ "data": { "productUpdate": { "id": "372390645805875406", "name": "New shoes", "price": 1000 } } }

We can also check that the validation on at least one input value being present by executing the following mutation:

mutation { productUpdate(by: { id: "372390645805875406" }, input: {}) { id name price } }

If everything "works" as expected, you should see the following error:

{ "data": null, "errors": [ { "message": "At least one field to update must be provided.", "locations": [ { "line": 2, "column": 3 } ], "path": ["productUpdate"] } ] }

In this step, we'll create a GraphQL mutation to delete products from the Fauna collection. To do this, update the grafbase.config.ts file to include the following:

const productDeletePayload = g.type('ProductDeletePayload', { deleted: g.boolean(), }) g.mutation('productDelete', { args: { by: g.inputRef(productByInput), }, resolver: 'products/delete', returns: g.ref(productDeletePayload).optional(), })

We don't need to add anything else to the configuration since we're reusing productByInput.

Now create the resolver file resolvers/products/delete.ts and add the following:

import { Client, FaunaError, fql } from 'fauna' const client = new Client() export default async function ProductsDelete(_, { by }) { const { id } = by try { const documentQuery = fql` products.byId(${id}).delete() ` await client.query(documentQuery) return { deleted: true } } catch (error) { if (error instanceof FaunaError) { console.log(error) } return { deleted: false } } }

That's it! Fauna will throw an error if the document by ID does not exist. You can forward these errors on by doing something like this inside the catch block:

if (error instanceof FaunaError) { throw new GraphQLError(error?.message) }

Open Pathfinder at http://127.0.0.1:4000 and execute the following GraphQL mutation:

mutation { productDelete(by: { id: "372397409740783822" }) { deleted } }

Make sure to use an id of a product that you created earlier.

You should now see something like this if the request was successful:

{ "data": { "productDelete": { "deleted": true } } }

We will now create a GraphQL query to fetch data from the Fauna Database. We'll be using the productByInput input type we created earlier.

Inside grafbase.config.ts add the following product query:

g.query('product', { args: { by: g.inputRef(productByInput) }, resolver: 'products/single', returns: g.ref(product).optional(), })

Next, create the file resolvers/products/single.ts and add the following:

import { Client, FaunaError, fql } from 'fauna' const client = new Client() export default async function ProductsSingle(_, { by }) { const { id } = by try { const documentQuery = fql` products.byId(${id}) { id, name, price } ` const { data } = await client.query(documentQuery) return data } catch (error) { if (error instanceof FaunaError) { console.log(error) } return null } }

Because by contains optional fields, we need to check if at least one was provided in the query. If no field was provided, we can throw a GraphQLError that will be returned to the client:

import { GraphQLError } from 'graphql' export default async function ProductsSingle(_, { by }) { const { id } = by if (Object.entries(by).length === 0) { throw new GraphQLError('You must provide at least one field to fetch by.') } // ... }

You're probably wondering why we don't just make id a required field. You could do that but it wouldn't allow us to add other fields we want to fetch "by" easily, such as a unique field.

GraphQL has a proposal for @oneOf input types which we could use here to reduce the boilerplate once it becomes officially part of the specification.

Now open Pathfinder at http://127.0.0.1:4000 and execute a GraphQL query to fetch an existing product by ID:

query { product(by: { id: "372397709974307023" }) { id name price } }

You should see something like this in the response:

{ "data": { "product": { "id": "372397709974307023", "name": "shoes", "price": 100 } } }

That's it! We can now create, update, delete and fetch products by ID. In the next step, we'll create a query to list all products.

We're now ready to finish the last piece of the GraphQL API by adding a new query that fetches all products from the Fauna Database.

Inside grafbase.config.ts add the following query definition:

g.query('products', { resolver: 'products/all', returns: g.ref(product).optional().list().optional(), })

Now create the file resolvers/products/all.ts and add the following:

import { Client, FaunaError, fql } from 'fauna' const client = new Client() export default async function ProductsAll() { try { const documentQuery = fql` products.all() { id, name, price } ` const { data } = await client.query(documentQuery) return data?.data || [] } catch (error) { if (error instanceof FaunaError) { console.log(error) } return [] } }

There's more we can do here to optimize the request to fetch all products and make sure of cursor-based pagination, but we'll do that in the next step.

Now open Pathfinder at http://127.0.0.1:4000 and execute the following GraphQL query:

query { products { id name price } }

You should see a response that contains your products and looks something like this:

{ "data": { "products": [ { "id": "372390606454915278", "name": "Shoes", "price": 1000 }, { "id": "372397709974307023", "name": "Cap", "price": 2000 } ] } }

Grafbase provides support for edge caching, a feature that enhances performance by serving already cached data, thereby eliminating the need to wait for a response from the database.

Inside grafbase.config.ts we will update the config export to include a new property: cache.

export default config({ graph: g, cache: { rules: [ { maxAge: 60, types: [{ name: 'Query', fields: ['products', 'product'] }], mutationInvalidation: 'entity', }, ], }, })

This configuration instructs Grafbase to set a maxAge to 60 seconds on the queries products and product.

The mutationInvalidation property also tells Grafbase to delete any cached data the entities returned tagged with their id value. This means if a Product is mutated and id matches any cached data tag, the data will be invalidated

The final step involves deploying your new GraphQL API to the Edge with the help of GitHub. Here's how you can do it:

    • Sign in to your GitHub account, or create one if you don't have it yet.
  1. Initiate a new repository via GitHub.
  2. In the Grafbase Dashboard, start a new project and link it with the repository you've just created.
  3. During the project setup, remember to add the necessary environment variables (FAUNA_SECRET).
  4. Now, deploy your project!

Once deployed, Grafbase will provide an API endpoint and an API key, which you can then utilize in your application.

To allow access from your frontend framework or browser, you'll need to configure and enable auth.

Get Started

Build your API of the future now.