Getting started with Fauna and Cloudflare Workers

Introduction

In this guide, we’re going to build a simple CRUD API to manage a catalog inventory. The code will be deployed worldwide on Cloudflare’s infrastructure and executed on the serverless Workers runtime, with Fauna as the data layer. Both services share the same serverless global DNA. When combined, they are an ideal platform to build low-latency services that can handle any workload with ease.

Prerequisites

Why use Fauna with Cloudflare Workers?

Thanks to its nonpersistent connection model based on HTTP, Fauna integrates seamlessly with Workers. Applications using both services can run serverless end to end and be infinitely scalable with no need to manage infrastructure. This setup is ideal for low-latency services.

Fauna and Cloudflare

Typically, server-side applications run their business logic and database operations on single central servers. By contrast, an application using Workers with Fauna runs in globally distributed locations and the round trip read latency is greatly reduced.

If you’d like to talk to a Fauna expert about using Cloudflare Workers with Fauna, please contact us. Otherwise, read on to dive into a hands-on exercise!

What we’re building

Our application consists of a JavaScript REST API with CRUD capabilities that manages a simple product inventory.

Fauna is a document-based database. Our product documents include the following data:

  • title: a human-friendly string representing the name of a product.

  • serialNumber: a machine-friendly string that identifies the product.

  • weightLbs: a floating point number with the weight in pounds of the product.

  • quantity: an integer number specifying how many of a product there are in the inventory.

The documents are stored in the Products collection. For simplicity’s sake, the endpoints of our API are public. Check some suggestions at the end of the guide on how to improve this.

Finally, we’ll use Cloudflare Workers to execute the JavaScript code of our application at the edge.

Set up Fauna

  1. Create a new database

    1. In the Fauna Dashboard, click the CREATE DATABASE link in the upper left corner.

    2. Name your database Cloudflare_Workers_Demo.

    3. In the Region Group dropdown menu, select your Region Group.

    4. Leave the Use demo data box unchecked.

    5. Click the CREATE button.

  2. Create a new collection

    1. Click the NEW COLLECTION button.

    2. Name your collection Products.

    3. Click the SAVE button.

  3. Create a new API key

    1. Click SECURITY in the left-side navigation.

    2. Click the NEW KEY button.

    3. Select Server from the Role dropdown menu.

    4. Click the SAVE button.

      Create a new API key

    5. Your key’s secret is displayed. Copy it and save it for later use. Be careful with your secret, and never commit it to a Git repository. Secrets are only ever displayed once. If you lose a secret, you must create a new key.

Your Fauna database is now ready to use.

Managing our inventory with Cloudflare Workers

  1. Set up Cloudflare

    If you haven’t already done so, create a Cloudflare account, enable Workers, and install the Wrangler CLI on your local development machine.

  2. Configure Wrangler

    1. Give Wrangler access to your Cloudflare account with the login command:

      wrangler login
    2. Create a Workers project with the generate command:

      wrangler generate

      generate creates a directory named worker with several files, including one named wrangler.toml which looks similar to the following:

      name = "worker"
      type = "javascript"
      
      account_id = ""
      workers_dev = true
      route = ""
      zone_id = ""
      compatibility_date = "2021-11-23"
    3. Edit wrangler.toml to add your Cloudflare account ID. You can find your account ID and Workers subdomain on your Workers dashboard screen.

  3. Create a test Worker

    1. To test your Cloudflare configuration, create a test Worker. Your worker directory contains all the necessary files. Publish your test Worker with the following terminal command:

      wrangler publish

      This creates and uploads the Worker to Cloudflare. The command response should display your Worker’s web address.

      Check your URL for a Hello worker! message. If it’s there, your configuration is correct. If not, re-check your account ID and subdomain on your Workers dashboard screen.

  4. Add your Fauna secret as an environment variable

    Once the Worker has been created and deployed, you can store your Fauna secret as an environment variable on Cloudflare’s infrastructure. Use the following terminal command:

    wrangler secret put FAUNA_SECRET

    At the prompt, paste the Fauna key secret you created earlier.

    It’s also possible to configure environment variables directly in the wrangler.toml file, but that’s a bad idea if you plan to add it to a Git repository.

  5. Install dependencies

    1. Install the Fauna JavaScript driver with the following terminal command:

      npm install faunadb
    2. Install the Worktop framework for Cloudflare Workers:

      npm install worktop@0.7

      Worktop provides basic functionality such as path parameters, query string parameters, and HTTP methods right out of the box.

  6. JavaScript utility functions

    Your CRUD API system needs a utility function for error handling. Create a file called utils.js in your project folder with the following content:

    export function getFaunaError(error) {
    
      const { code, description } = error.requestResult.responseContent.errors[0];
      let status;
    
      switch (code) {
        case 'unauthorized':
        case 'authentication failed':
          status = 401;
          break;
        case 'permission denied':
          status = 403;
          break;
        case 'instance not found':
          status = 404;
          break;
        case 'instance not unique':
        case 'contended transaction':
          status = 409;
          break;
        default:
          status = 500;
      }
    
      return { code, description, status };
    }

    The getFaunaError() function extracts the code and description of the most common errors returned by Fauna. It also determines the HTTP status code for each error.

  7. Inventory logic

    Replace the contents of your index.js file with the skeleton of an API:

    import {Router, listen} from 'worktop';
    import faunadb from 'faunadb';
    import getFaunaError from './utils.js';
    
    const router = new Router();
    
    const faunaClient = new faunadb.Client({
      secret: FAUNA_SECRET,
      domain: 'db.fauna.com',
      // NOTE: Use the correct domain for your database's Region Group.
    });
    
    const q = faunadb.query;
    
    listen(router.run);

    Let’s take a closer look at the initialization of the Fauna client:

    const faunaClient = new faunadb.Client({
      secret: FAUNA_SECRET,
      domain: 'db.fauna.com',
      // NOTE: Use the correct domain for your database's Region Group.
    });

    The FAUNA_SECRET environment variable is automatically injected into our application at runtime. Workers runs on a custom JavaScript runtime instead of Node.js, so there’s no need to use process.env to access these variables. The Fauna secret you uploaded belongs to a key with the Server role, which gives our Worker full access to your Fauna database.

  8. Add a POST API endpoint

    The POST endpoint receives requests to add documents to your Products collection. Add the following lines to your index.js file:

    router.add('POST', '/products', async (request, response) => {
      try {
        const {serialNumber, title, weightLbs} = await request.body();
    
        const result = await faunaClient.query(
          q.Create(
            q.Collection('Products'),
            {
              data: {
                serialNumber,
                title,
                weightLbs,
                quantity: 0
              }
            }
          )
        );
    
        response.send(200, {
          productId: result.ref.id
        });
      } catch (error) {
        const faunaError = getFaunaError(error);
        response.send(faunaError.status, faunaError);
      }
    });

    For simplicity’s sake, there’s no validation of the request input on these examples.

    This route contains a JavaScript FQL query which creates a new document in the Products collection. If the query is successful, the id of the new document is returned in the response body.

    If Fauna returns an error, the client raises an exception, and the program catches that exception and responds with the result from the getFaunaError() utility function.

  9. Add a GET API endpoint

    The GET endpoint retrieves a single document in your Products collection by its ID. Add the following lines to your index.js file:

    router.add('GET', '/products/:productId', async (request, response) => {
      try {
        const productId = request.params.productId;
    
        const result = await faunaClient.query(
          q.Get(q.Ref(q.Collection('Products'), productId))
        );
    
        response.send(200, result);
    
      } catch (error) {
        const faunaError = getFaunaError(error);
        response.send(faunaError.status, faunaError);
      }
    });

    The FQL query uses the Get function to retrieve a document from a document reference. If the document exists, it is returned in the response body.

  10. Add a DELETE API endpoint

    The DELETE endpoint deletes a single document in your Products collection by its ID. Add the following lines to your index.js file:

    router.add('DELETE', '/products/:productId', async (request, response) => {
    
      try {
        const productId = request.params.productId;
    
        const result = await faunaClient.query(
          q.Delete(q.Ref(q.Collection('Products'), productId))
        );
    
        response.send(200, result);
    
      } catch (error) {
        const faunaError = getFaunaError(error);
        response.send(faunaError.status, faunaError);
      }
    });

    The FQL query uses the Delete function to delete a document by its reference. If the delete operation is successful, the deleted document is returned in the response body.

  11. Add a PATCH API endpoint

    The PATCH endpoint updates a single document in your Products collection by its ID. You’ll use it to manage your product inventory by reading and updating the quantity field in your product documents.

    Keeping your product inventory up to date involves two database operations: reading the current quantity of an item, and then updating it to a new value. If you perform these two operations separately, you run the risk of getting out of sync, so it’s better to perform them as part of a single transaction.

    Add the following lines to your index.js file:

    router.add('PATCH', '/products/:productId/add-quantity', async (request, response) => {
    
      try {
        const productId = request.params.productId;
        const {quantity} = await request.body();
    
        const result = await faunaClient.query(
          q.Let(
            {
              productRef: q.Ref(q.Collection('Products'), productId),
              productDocument: q.Get(q.Var('productRef')),
              currentQuantity: q.Select(['data', 'quantity'], q.Var('productDocument'))
            },
            q.Update(
              q.Var('productRef'),
              {
                data: {
                  quantity: q.Add(
                    q.Var('currentQuantity'),
                    quantity
                  )
                }
              }
            )
          )
        );
    
        response.send(200, result);
    
      } catch (error) {
        const faunaError = getFaunaError(error);
        response.send(faunaError.status, faunaError);
      }
    });

    Let’s examine the FQL query in this route in more detail.

    It uses the Let function to establish some variables you’ll need later on:

    • productRef is a document reference.

    • productDocument contains the full product document, returned by the Get function.

    • currentQuantity contains the value from the quantity field of the document. The Select function extracts the quantity field from the specified document.

      The variables created by Let are available to any subsequent FQL expressions with the Var function.

      After declaring the variables, Let accepts a second parameter which can be any FQL expression. This is where we you update the document:

      q.Update(
        q.Var('productRef'),
        {
          data: {
            quantity: q.Add(
              q.Var('currentQuantity'),
              quantity
            )
          }
        }
      )

      The Update function only updates the specified fields of a document. In this example, only the quantity field is updated. The new value of quantity is calculated with the Add function.

      It’s important to note that even if multiple Workers are updating product quantities from different geographical locations, Fauna guarantees the consistency of the data across all Fauna regions.

      Now you have API routes to create, delete, retrieve, and update documents. You can now publish your Worker and try it out.

  12. Publish your Worker

    Publish your Worker with the following terminal command:

    wrangler publish
  13. Try it out

    Create a new product document with the following terminal command. Replace <worker-address> with the HTTPS address of your Workers account, which you can find on your Workers dashboard. It should look something like cloudflare.my-subdomain.workers.dev.

    curl \
        --data '{"serialNumber": "H56N33834", "title": "Bluetooth Headphones", "weightLbs": 0.5}' \
        --header 'Content-Type: application/json' \
        --request POST \
        https://<worker-address>/products

    If the request is successful, you should get a response body which contains the product ID of your new document:

    {"productId":"315195527768572482"}

    You can go to your Fauna Dashboard and look at your Products collection to see the new document. You can also send a GET request to retrieve the new document. Replace <worker-address and <document-id> with the correct values:

    $ curl \
        --header 'Content-Type: application/json' \
        --request GET \
        https://<worker-address>/products/<document_id>

    Next, let’s use the PATCH endpoint to update the quantity field of the document.

    $ curl \
        --data '{"quantity": 5}' \
        --header 'Content-Type: application/json' \
        --request PATCH \
        https://<worker-address>/products/<document_id>/add-quantity

    The above terminal command should return the complete product document with an updated quantity field. The terminal output isn’t formatted for easy reading, but you can always return the Fauna Dashboard and look at your document there.

    Finally, let’s use the DELETE endpoint to delete the document.

    $ curl \
        --header 'Content-Type: application/json' \
        --request DELETE \
        https://<worker-address>/products/<document_id>

Further improvements to the application

Your inventory application has been kept very simple for demonstration purposes, but it lacks essential features.

For example, all endpoints of the API are public. A real-world application makes use of authentication and authorization to maintain security. Here are some resources for learning about application security with Fauna.

Another important improvement would be to add some reporting capabilities. To accomplish this requires a deeper understanding of Fauna indexes and data aggregation.

Is this article helpful? 

Tell Fauna how the article can be improved:
Visit Fauna's forums or email docs@fauna.com

Thank you for your feedback!