---
title: "Building an Edge API Gateway with Fauna and Securing It with Auth0"
description: "In this tutorial, we’ll explore architecting REST APIs in a fully serverless manner by leveraging Fastly’s Compute@Edge, Fauna, and Auth0"
authors:
  - name: "Zee Khoo"
    url: "https://auth0.com/blog/authors/zee-khoo/"
date: "Dec 21, 2021"
category: "Developers,Tutorial,Fauna"
tags: ["fauna", "db", "auth0"]
url: "https://auth0.com/blog/building-an-edge-api-gateway-with-fauna-and-securing-it-with-auth0/"
---

# Building an Edge API Gateway with Fauna and Securing It with Auth0

When building an application, ensuring a great user experience is a key to user engagement, ultimately impacting how happy your users are with your product. A critical aspect of how a user experiences your application is latency. This is especially true when end-users are globally distributed, as latency is dependent on the distance between the end-user and where the application is being served.

Yesterday’s applications didn’t need to provide millisecond response times for users anywhere in the world, so most compute and database options found in production today cannot handle the use case. But with edge computing, there may finally be an easy way to move applications closer to the end-user, if only databases were quick to adapt to this new architecture. If you’re building an API today, you cannot simply “deploy” any database at the edge like the picture shown below, as the edge cannot fully replace the function of a true server.

![Databases Are Meant To Be Connected To Servers.drawio](https://images.ctfassets.net/23aumh6u8s0i/67auSB1QgE4vXfTty0LrkL/754afaac91ac10e814bad62a34ed7127/Uploaded_from_Building_an_edge_API_gateway_with_Fauna_and_securing_it_with_Auth0)

So what you’re left with is placing the API (still requiring the app server, database, and all) behind the edge, in the configuration shown below:

![better](https://images.ctfassets.net/23aumh6u8s0i/2FdjMAEVIlFKASbIqLqHf8/b7862361c1e09bbfa0ca9c2ad9618e7b/Uploaded_from_Building_an_edge_API_gateway_with_Fauna_and_securing_it_with_Auth0)

This is still an improvement, as it adds resiliency and reduces latency by caching results where possible. But if speed is the goal, can you do better? Notice the database still has to sit somewhere relative to the edge locations everywhere. What if the database is far from the user? Engineers have solved this by implementing globally [sharded](https://www.digitalocean.com/community/tutorials/understanding-database-sharding) databases. But that’s complicated and expensive.

If you have the luxury of starting fresh and want to realize the full promise of scale, resiliency, and performance that edge computing provides, you need a globally distributed, serverless database that edge functions simply access using [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch). This is where [Fauna](https://fauna.com/) comes into the picture.

![Edge to Cloud API.drawio](https://images.ctfassets.net/23aumh6u8s0i/kq1p1t1Cp2wAzs3o6aVdm/912fefb267f3a76db421694502a1d2dc/Uploaded_from_Building_an_edge_API_gateway_with_Fauna_and_securing_it_with_Auth0)

## What We’re Building

> See the full sample of this [tutorial’s content](https://github.com/fauna-labs/edge-gateway-sample-fastly).

In this tutorial, we’ll explore architecting REST APIs in a fully serverless manner by leveraging [](https://www.fastly.com/products/edge-compute/serverless)edge computing and Fauna, and we’re going to secure the API requests with Auth0. In the interest of emphasizing speed, we’ll use Fastly’s Compute@Edge, which provides ultralow startup times and a low memory footprint via its WASI runtime. The examples below will walk you through building a user registration flow for a website. You’ll be implementing the /users resource and its GET, POST, and PUT methods.

![C@E demo registration.drawio](https://images.ctfassets.net/23aumh6u8s0i/pwn6amsnHoKAieO3LzHVH/2980da0f6f919c8b3ba5061395335843/Uploaded_from_Building_an_edge_API_gateway_with_Fauna_and_securing_it_with_Auth0)

We’ll also demonstrate two powerful features of Fauna:

1.  With [Region Groups](https://docs.fauna.com/fauna/current/api/fql/region_groups) you can create your databases in geographic regions of your choice. You can then easily build GDPR compliant architectures by routing requests to specific region groups — with the help of the edge — depending on where the user is making the request from. The performance also benefits by having the request from the client to edge to the database being as close to each other as possible.
2.  Fauna supports [external authentication](https://docs.fauna.com/fauna/current/security/external/) with any Identity Provider (IdP) that supports the OpenID Connect protocol, allowing you to leverage a service such as [Auth0](https://auth0.com/) so that you can focus on developing features instead of worrying about security.

![Fastly Fauna Auth0.drawio](https://images.ctfassets.net/23aumh6u8s0i/30QRsNGnCJtPorHVzoMaUB/65e172cb8cebe11ecd9dc0ab49135f7f/Uploaded_from_Building_an_edge_API_gateway_with_Fauna_and_securing_it_with_Auth0)

## What’s Needed to Complete This Tutorial?

*   An [Auth0](https://auth0.com/sign-up) account
*   A [Fauna](https://dashboard.fauna.com/accounts/register) account
*   A [Fastly](https://www.fastly.com/signup/) account
*   An API Client such as [Postman](https://www.postman.com/) or curl
*   [Node.js](https://nodejs.org/en/) installed on your system

...and nothing else! No infrastructure is required because you won’t be standing up or deploying any servers.

## Setting up Fauna

Your first step will be to configure a database through Fauna's dashboard. [Login to the dashboard](https://dashboard.fauna.com/), and create a new database named _client-serverless-api_:

![fastly_create_database_eu450](https://images.ctfassets.net/23aumh6u8s0i/58Z4Tv7J4SYOaPnfqKdH0m/ef9c60ff3a6cb87b2266c9e384086b41/Uploaded_from_Building_an_edge_API_gateway_with_Fauna_and_securing_it_with_Auth0)

*   For Region Group, choose Europe (EU) (for the sake of demonstrating Fauna’s multi Region Group capability).

### Create the User Collection

From the left navigation, select Collections and click [NEW COLLECTION](https://fauna.com/blog/how-to-build-an-edge-api-gateway-with-fastlys-compute-edge-and-fauna). Provide a name, i.e. _User_ and click [SAVE](https://fauna.com/blog/how-to-build-an-edge-api-gateway-with-fastlys-compute-edge-and-fauna).

![fastly_create_new_fauna_collection](https://images.ctfassets.net/23aumh6u8s0i/1td3iOxeISeuIwuJBUWL2y/11f96a84a7982dafc000dcef8e082611/Uploaded_from_Building_an_edge_API_gateway_with_Fauna_and_securing_it_with_Auth0)

### The user model

Below is the design of our _User_ model:

```
{
"user\_id": "57a9160561254be4229bca97",
"email": "rainer.theo@mailinator.com"
"profile": {
"name": "Rainer Theo",
"countryCode": "DE",
"age": "23"
}
}
```

*   `user_id` &mdash; stores the Auth0 user id
*   `email` &mdash; A required field
*   `profile` &mdash; An optional field freeform JSON object

### Create an index

There will be a `GET /users/{id}` and a `POST /users/{id}` API. So you’ll need to create an index which will allow you to query by user\_id.

_In the interest of learning, we’ll execute all our Fauna commands in the shell going forward, even though some resource configurations (such as creating and updating Collections and Indexes) are available using the UI._

From your Fauna dashboard, use the left navigation and select Shell. Enter the following query into the shell at the bottom of the screen then click [RUN QUERY](https://fauna.com/blog/how-to-build-an-edge-api-gateway-with-fastlys-compute-edge-and-fauna).

```
CreateIndex({
  name: "user_by_id",
  source: Collection("User"),
  terms: [
    { field: ["data", "user_id"] }
  ],
  unique: true
})
```

_Since `user_id` should be unique, we specify the `unique: true` argument._

### User defined functions

![REST to UDF.drawio](https://images.ctfassets.net/23aumh6u8s0i/39zicypQMxWFOZmV596lmB/b2a908110a56fdbff4d2ebb0390cfacf/Uploaded_from_Building_an_edge_API_gateway_with_Fauna_and_securing_it_with_Auth0)

The feature-rich, expressive nature of FQL combined with the ability to save them as UDFs makes our implementation straightforward. The key approach will be to map each of the three API calls to their own UDF and then push as much logic as possible down to Fauna (the UDF) so that code running at the edge is really lightweight.

Let's start with `POST /users`, which we spec out as such:

POST /users

Parameters: JSON body containing `email` (optional) and an embedded profile `JSON` (optional). Headers: Authorization Bearer {{token}} _Request example:_

```
curl -X POST -H "Content-Type: application/json" \
-H "Authorization: Bearer eyJhb..." \
-d '{"email":"rainer.theo@mailinator.com", "profile": { "name": "Rainer Theo" } }' \

https://example.domain.eu/api/users
```

_Response example:_

```
{
"user\_id": "57a9160561254be4229bca97",
"email": "rainer.theo@mailinator.com",
"profile": {
"name": "Rainer Theo"
}
}
```

*   `user_id` is automatically populated using the `sub` (subject) claim in the bearer token. Any `user_id` value provided in a `POST /users` request body will be ignored.
*   Auth0 populates `sub` with its internal `user_id`, e.g. `auth0|57a9160561254be4229bca97`.

The first thing you’ll do is create your first role. Custom roles can be defined and attached to UDFs, providing them permissions to operate on resources. You’ll need a role that can _read_, _write,_ and _create_ _Users_ (you’ll then attach this role to the “Create User” UDF). Run the following query:

```
CreateRole({
  name: "usersCRUD",
  privileges: [
    {
      resource: Collection("User"),
      actions: {
        read: true,
        write: true,
        create: true,
      }
    },
    {
      resource: Index("user_by_id"),
      actions: {
        unrestricted_read: true
      }
    }    
  ]
})
```

*   We’re also going to need this role later on when we implement GET and PUT, so it should also have the permission to read the _user_by_id_ index.

Now, run this query to create the _CreateUser_ UDF:

```
CreateFunction({
  name: "CreateUser",
  body: Query(
    Lambda(
      ["body"],
      Let(
        {
          email: Select(["email"], Var("body"), null),
          profile: Select(["profile"], Var("body"), null),
          dataForCreate: {
            user_id: CurrentIdentity(),
            email: Var("email"),
            profile: Var("profile")
          }
        },
        Do(
          Create(Collection("User"), { data: Var("dataForCreate") }),
          Var("dataForCreate")
        )
      )
    )
  ),
  role: Role("usersCRUD")
})
```

*   When you authenticate Fauna with a JWT (External Authentication), the CurrentIdentity() function returns the `sub` claim of the JWT.
*   FQL allows us to return precise responses to the caller. Because we want the API response to look like what we defined in our spec, we call `Var("dataForCreate")` right at the end. Otherwise, we'll get the standard response of `Create()`, which won't look the same.

### Auth0 AccessProvider setup

You’re almost ready to test _CreateUser_, but first, you’ll need a JWT from Auth0 and there’s an integration to complete.

Step 1: Create an AccessProvider in Fauna: Create a role that has access to call the _CreateUser_ function:

```
CreateRole({
  name: "APIsRole",
  privileges: [
    {
      resource: Function("CreateUser"),
      actions: {
        call: true
      }
    }
  ],
})
```

_This sets up a custom role for our Auth0 integration, which we’ll need when configuring the Auth0 AccessProvider._

Next, use the UI to create an AccessProvider: From the left navigation panel, select Security > Providers. Then click [NEW ACCESS PROVIDER](https://fauna.com/blog/how-to-build-an-edge-api-gateway-with-fastlys-compute-edge-and-fauna).

![fastly_Auth0_For_EU650](https://images.ctfassets.net/23aumh6u8s0i/5KHU1ex6QdAZFGobzdceKs/9007112b4abc04aeaaaf245d328277b9/Uploaded_from_Building_an_edge_API_gateway_with_Fauna_and_securing_it_with_Auth0)

*   Provide a name. e.g. Auth0-For-EU.
*   Copy the Audience value for "Step 2" (after this section).
*   Set Issuer = `https://<<your auth0 domain>>.auth0.com/` and JWKS endpoint = `https://<<your auth0 domain>>.auth0.com/.well-known/jwks.json` and replace `<<your auth0 domain>>` with your own Auth0 domain name. (Your Auth0 domain can be found in the top left-hand corner of your Auth0 dashboard. See screenshot below.)

![Activity](https://images.ctfassets.net/23aumh6u8s0i/54oBkKLxxdSiQonJJZHGgJ/e9e50c295e775b4be334191c237fa2b6/Uploaded_from_Building_an_edge_API_gateway_with_Fauna_and_securing_it_with_Auth0)

_Note: Depending on the version of your Auth0 account, you may have a subdomain variation that includes region, e.g. `https://mydomain.us.auth0.com/`_

*   Select the APIsRole role (from the previous step) where the “Select a role” dropdown is.
*   Click [SAVE](https://fauna.com/blog/how-to-build-an-edge-api-gateway-with-fastlys-compute-edge-and-fauna).

Step 2: Setup an API in Auth0:

Sign in to Auth0. From the left navigation panel, select Applications > APIs. Provide a Name, e.g. _client-serverless-api,_ and paste the Audience value copied in the previous step into the Identifier field. Click [Create](https://fauna.com/blog/how-to-build-an-edge-api-gateway-with-fastlys-compute-edge-and-fauna).

![fastly_auth0_new_api450](https://images.ctfassets.net/23aumh6u8s0i/7mzPHu0xafaWBTSBtHSko1/df05bf68f2b942afe9fd375f9562e3b1/Uploaded_from_Building_an_edge_API_gateway_with_Fauna_and_securing_it_with_Auth0)

Once that API is created, navigate to the Test tab. Copy the value of access\_token from the sample response (see screenshot below), then head back to the Fauna shell.

![fastly_testJWT](https://images.ctfassets.net/23aumh6u8s0i/7Dky634AN2ahbpWSgkkfA8/5027d2959b83ad63e5a1e7277665c967/Uploaded_from_Building_an_edge_API_gateway_with_Fauna_and_securing_it_with_Auth0)

> Note: When you created the API, Auth0 automatically created a test application that can use the `client_credentials` grant, which allows generation of `access_tokens` without a user logging in. The `sub` (subject) claim in the access_token above refers to the test application instead of any user and you’ll notice that it has a different format. e.g. `uf7TdWaOKyWvLahkO2oxJi0AWBdVg24K@clients.`

Step 3: Use the sample access_token in the Fauna shell

At the bottom of the screen, next to the [RUN QUERY](https://fauna.com/blog/how-to-build-an-edge-api-gateway-with-fastlys-compute-edge-and-fauna) button, click “RUN AS” and change the value of the dropdown from “Admin” to “Specify a secret.“ Paste the access_token copied from the previous step into the box next to it, as shown below:

![Run query as](https://images.ctfassets.net/23aumh6u8s0i/3MeA1NHXzY4wNiTAWkZnCL/bb2cc716b22d59c2a7282f9a5f3e3ad2/Uploaded_from_Building_an_edge_API_gateway_with_Fauna_and_securing_it_with_Auth0)

Test your _CreateUser_ UDF by executing this query:

```
Call(
  Function('CreateUser'), 
  {
    email: "rainer.theo@mailinator.com",
    profile: {
      name: "Rainer Theo",
      country: "DE"
    }
  }
)
```

Here’s a sample response:

```
{
  user_id: "uf7TdWaOKyWvLahkO2oxJi0AWBdVg24K@clients",
  email: "rainer.theo@mailinator.com",
  profile: {
    name: "Rainer Theo",
    country: "DE"
  }
}
```

You should see a response similar to the above, confirming that the UDF is working. To be certain that it created the User, navigate to Collections, then click on User to inspect the collection. The UI allows us to preview all its documents. Notice that a new _User_ document has been successfully created. This should confirm that the UDF is working.

![User document](https://images.ctfassets.net/23aumh6u8s0i/3Fh0nSnxVSR8GlWtyeApEz/b02b3ed155116ecb611576fdd3a63d6a/Uploaded_from_Building_an_edge_API_gateway_with_Fauna_and_securing_it_with_Auth0)

### Compute@Edge

With the UDF and Auth0 integration completed, let’s finally get down to implementing the POST /users endpoint (and take it live) with Compute@Edge.

Create a service

[Sign in](https://manage.fastly.com/auth/sign-in) to Fastly. Navigate to the “Compute“ tab at the top of the screen and click [Create a Compute service](https://fauna.com/blog/how-to-build-an-edge-api-gateway-with-fastlys-compute-edge-and-fauna).

![fastl_create_a_computer_service](https://images.ctfassets.net/23aumh6u8s0i/4AYecaU7ypmzxKA6cPb7fy/8d5dd4001c768e067c02ef1205119dbe/Uploaded_from_Building_an_edge_API_gateway_with_Fauna_and_securing_it_with_Auth0)

After the screen updates, update the default name by clicking on it. Change it to something descriptive, e.g. `client-serverless-api` as we’re going to reference it again later.

![fastly_update_service_name](https://images.ctfassets.net/23aumh6u8s0i/13iFGgnqNAqHQYlsgg3qqG/5a68d68f563087f52fe88fda865f42e3/Uploaded_from_Building_an_edge_API_gateway_with_Fauna_and_securing_it_with_Auth0)

Click on the “Service configuration” tab. From there, use the left navigation pane and select Origins > Hosts. In the empty field, add the value `db.eu.fauna.com` and click [Add](https://fauna.com/blog/how-to-build-an-edge-api-gateway-with-fastlys-compute-edge-and-fauna). Doing this whitelists Fauna’s EU domain for use as a [valid host](https://docs.fastly.com/en/guides/working-with-hosts).

![fastly_origins_hosts](https://images.ctfassets.net/23aumh6u8s0i/50nox3NWpt6qIlH4Y0QPH6/bacb514fd04e2a94231bfc086c650b8f/Uploaded_from_Building_an_edge_API_gateway_with_Fauna_and_securing_it_with_Auth0)

After adding the host, it is given a default name that you’ll now override to something more descriptive, since later on in the tutorial, you’ll be referencing this value. To edit the name, first click on it to enter edit mode. Enter the new value `db_eu_fauna_com` then scroll to the bottom of the screen and click [Update](https://fauna.com/blog/how-to-build-an-edge-api-gateway-with-fastlys-compute-edge-and-fauna).

![fastly_edit_this_host_600](https://images.ctfassets.net/23aumh6u8s0i/2HG1XeNVqPorFGi9GMquiF/b34e6d8978583fe5eced1368baccdfe8/Uploaded_from_Building_an_edge_API_gateway_with_Fauna_and_securing_it_with_Auth0)

Get a personal API token

From the top right corner of your Fastly dashboard, click on your username to activate a dropdown menu. Choose “Account” to enter the Account Settings page. Use the left navigator panel and select Personal API Tokens. Click [Create Token](https://fauna.com/blog/how-to-build-an-edge-api-gateway-with-fastlys-compute-edge-and-fauna) and configure these settings:

*   Provide a name, e.g. client-serverless-api.
*   Service Access = A specific service. From the dropdown, select the service you created earlier.
*   Scope = Global API access
*   Expiration = Never expire

![Create a token](https://images.ctfassets.net/23aumh6u8s0i/1fbUjmjFCs49bIn5rPDj8Q/d64b164b5ea0855949f2e7cfdf622712/Uploaded_from_Building_an_edge_API_gateway_with_Fauna_and_securing_it_with_Auth0)

*   Click [Create Token](https://fauna.com/blog/how-to-build-an-edge-api-gateway-with-fastlys-compute-edge-and-fauna). On the next screen, copy the generated value and save it somewhere safe for future reference. You won’t be able to access this value again.

![fastly_personal_api_token400](https://images.ctfassets.net/23aumh6u8s0i/3tFfrX2VMZDlLX2buefrIR/68e023aa2ff167c043335235ec1c5968/Uploaded_from_Building_an_edge_API_gateway_with_Fauna_and_securing_it_with_Auth0)

Use the Fastly CLI and edit the service handlers

Install the [Fastly CLI](https://github.com/fastly/cli). Create a project directory and `cd` into it. Run `fastly compute init` and follow the prompts to create a project.

*   When prompted for Language, choose Javascript.
*   When prompted for Starter kit, select the Default.Open up your favorite IDE to the project that was cloned into the directory. Within the project, create a file `utils.js` in the `/src` folder with the following content:

```js
/* 
 * Fauna embeds its own error “codes” (actually string text) in the response body. 
 * This function parses out the error codes and translate it back to HTTP error codes.
 */
export function getFaunaError(response) {
  try {
    const errors = response.errors[0]
    let { code, description, cause } = errors;
    let status;

    try {
      // report on the inner errors if they exist
      code = cause[0].code;
      description = cause[0].description;
      if (code == 'transaction aborted') {
        // Inside UDFs use 'transaction aborted' status to bubble up the actual error code in the description.
        code = description;
      }
    } catch {
      // no error causes
    }

    switch (code) {
      case 'instance not found':
        status = 404;
        break;
      case 'instance not unique':
        status = 409;
        break;
      case 'permission denied':
        status = 403;
        break;
      case 'unauthorized':
      case 'authentication failed':
        status = 401;
        break;
      default:
        status = 500;
    }
    return { code, description, status };
  } catch {
    // no errors in response
    return false;
  }
}

/*
 * Doesn’t do much for now. We'll come back here later when we
 * utilize Geo-IP at the edge functionality 
 */
export function resolveBackend(request) {
  try {
    const bearerToken = request.headers.get('Authorization').split('Bearer ')[1];

    let backend = 'db_eu_fauna_com';
    let backendUrl = 'https://db.eu.fauna.com';

    return { backend, backendUrl, bearerToken };
  } catch (e) {
    console.log(`${e}`);
    throw e;
  }
}

/*
 * Fauna’s UDF needs to distinguish arguments between scalar and object types.
 * Objects must be wrapped with "object".
 * Example: a UDF input argument of type object:
 * {
 *   foo: { 
 *     bar: {
 *        key: 'value'
 *     }
 *   }
 * }
 * ...must be formatted for REST call:
 * object: {
 *   foo: {
 *     object: {
 *       bar: {
 *         object: {
 *           key: 'value'
 *         }
 *       }
 *     }
 *   }
 * }
 */
export function wrapWithObject(obj) {
  let result = {};
  for (const [key, value] of Object.entries(obj)) {
    if (typeof value === 'object') {
      result[key] = {
        object: wrapWithObject(value)
      }
    } else {
      result[key] = value;
    }
  }
  return result;
}

/* 
 * Translates Call(Function('name')) to REST
 */
export function formatFaunaCallFunction(functionName, id, requestBody) {
  let payload = {
    call: { function: functionName },
    arguments: []
  };
  if (id) {
    payload.arguments.push(id);
  }
  if (requestBody) {
    payload.arguments.push({ object: wrapWithObject(requestBody) });
  }
  return payload;
}

export function badRequest() {
  return new Response('Bad request', { 
    headers: { "access-control-allow-origin": "*" },
    status: 400 
  });
}
````

Then overwrite the file `/src/index.js` with these contents. Here’s where everything comes together.

```js
import {
  badRequest, getFaunaError,
  resolveBackend, formatFaunaCallFunction
} from './utils.js';

addEventListener('fetch', event => event.respondWith(handleRequest(event)));

async function handleRequest(event) {
  const req = event.request;

  const VALID_METHODS = ["GET", "POST", "PUT"];
  if (!VALID_METHODS.includes(req.method)) {
    const response = new Response("This method is not allowed", {
      status: 405
    });
    return response;
  }

  const method = req.method;
  const url = new URL(event.request.url);
  const pathname = url.pathname;

  // POST /users
  if (method == "POST" && pathname == "/users") {
    try {
      const reqBody = await req.json();
      if (!reqBody.email) {
        return new Response('Email is required', { status: 400 });
      }      
      return await callUDF(req, () => {
        return formatFaunaCallFunction('CreateUser', null, reqBody);
      });
    } catch {
      return badRequest();
    }
  }

  return new Response("The page you requested could not be found", {
    status: 404
  });
};

async function callUDF(request, formatHandler) {
  try {
    const { backend, backendUrl, bearerToken } = resolveBackend(request);

    // formatHandler translates REST request into FQL "Call(Function('name'))" equivalent
    const body = formatHandler();

    const headers = new Headers({
      "Authorization": `Bearer ${bearerToken}`,
      "Content-Type": "application/json"
    });

    const faunaRest = new Request(backendUrl, {
      method: "POST",
      headers: headers,
      body: JSON.stringify(body)
    });

    const res = await fetch(faunaRest, { backend: backend });

    let response = await res.json();

    // If FQL throws an error, return error
    const faunaErrors = getFaunaError(response);
    if (faunaErrors) {
      return new Response(
        faunaErrors.description, {
        headers: { 
          "content-type": "application/json;charset=UTF-8",
          "Access-Control-Allow-Origin": "*"
        },          
        status: faunaErrors.status
      });
    } else {
      return new Response(
        JSON.stringify(response.resource), {
        headers: { 
          "content-type": "application/json;charset=UTF-8",
          "Access-Control-Allow-Origin": "*"
        },
        status: 200
      });
    }
  } catch (e) {
    console.log(`${e}`);
    return new Response(`${e}`, { status: 500 });
  }
}
```

Deploy the service

Set the environment variable `FASTLY_API_TOKEN` to the Personal API Token obtained previously.

```
export FASTLY_API_TOKEN=<<token>>
```

In your Fastly dashboard, locate the service ID (see the screenshot below for reference) then update the `fastly.toml` file and set the `service_id` to this value.

![fastly_ServiceId](https://images.ctfassets.net/23aumh6u8s0i/4kHgqigPwVeJW8kJfNIauq/33cba121f1bd37ecaa6b71993b80de51/Uploaded_from_Building_an_edge_API_gateway_with_Fauna_and_securing_it_with_Auth0)

Build the project:

```
fastly compute build
```

Then deploy the service with this command:

```
fastly compute deploy
```

*   When prompted to provide a domain, click Enter to accept the generated value. Wait a few seconds for the service to deploy. You should see the progress on the terminal. Once deployed (give it about 30 seconds to fully propagate), test out the live API.

### Test your API

Back at your Fauna dashboard, from the left navigator pane, select Collections then click User. Next to the user record you created earlier, there should be a delete icon. Go ahead and delete that user. We’ll need to do this because `user_id` is unique, and we’re about to test with the same access_token.

![Delete user](https://images.ctfassets.net/23aumh6u8s0i/4ApQek9WQ0AK1aBA4n7567/2b7eea338aef626d558d477615b19582/Uploaded_from_Building_an_edge_API_gateway_with_Fauna_and_securing_it_with_Auth0)

Call the live API with your tool of choice, e.g. Postman, or curl:

```
curl --location --request POST 'https://frequently-faithful-mouse.edgecompute.app/users' \
--header 'Authorization: Bearer eyJhbGciOiJ...' \
--header 'Content-Type: application/json' \
--data-raw '{
    "email": "rainer.theo@mailinator.com",
    "profile": {
        "name": "Rainer Theo",
        "city": "Frankfurt",
        "country": "DE"
    }
}'
```

If the request goes through, check the response to see if it matches our spec. Then check the Fauna dashboard to see if the Users document was created successfully.

If everything checks out we’re ready to move on to the next API. So let’s spec out `GET /users` and `PUT /users`:

GET /users/{id}

_Parameters:_

*   `{id}` (Required)
    *   Must match the “user_id” embedded in the access_token’s `sub` claim (_users can read themselves_).

_Headers:_

*   Authorization Bearer {{token}}

_Request Example:_

```
curl -H "Authorization: Bearer eyJhb..." \

https://frequently-faithful-mouse.edgecompute.app/users/auth0%7C57a9160561254be4229bca97
```

_Response example:_

```
{
  "user_id": "auth0|57a9160561254be4229bca97",
  "email": "rainer.theo@mailinator.com"
  "profile": {
    "name": "Rainer Theo",
    "country": "DE",
    "age": "23"
  } 
}
```

PUT /users/{id}

_Parameters:_

*   `{id}` (Required). Must match the “user\_id” embedded in the access_token’s `sub`claim (_users can update only themselves_).
*   JSON body of our User schema.

_Headers:_

*   Authorization Bearer {{token}}

_Request Example:_

```
curl -X PUT  -H "Content-Type: application/json" \
     -H "Authorization: Bearer eyJhb..."  \
     -d '{"email":"rainer.theo@mailinator.com", "profile": { "name": "Rainer Theo", "country": "DE", "age": 30 } }' \
     https://frequently-faithful-mouse.edgecompute.app/users/auth0%7C57a9160561254be4229bca97
```

_Response example:_

```
{
  "user_id": "auth0|57a9160561254be4229bca97",
  "email": "rainer.theo@mailinator.com"
  "profile": {
    "name": "Rainer Theo",
    "country": "DE",
    "age": 30
  } 
}
```

Back in your Fauna dashboard. Navigate to the shell and change the run [RUN QUERY AS](https://fauna.com/blog/how-to-build-an-edge-api-gateway-with-fastlys-compute-edge-and-fauna) back to Admin so that you can define these next UDFs.

![fastly_run_as_admin45h](https://images.ctfassets.net/23aumh6u8s0i/6PejHiffD32IAbvTThCiew/a8d3259ae42796ccd102d187abde46ab/Uploaded_from_Building_an_edge_API_gateway_with_Fauna_and_securing_it_with_Auth0)

Now define the _GetUser UDF_:

```
CreateFunction({
  name: 'GetUser',
  body: Query(
    Lambda(
      ["ID"],
      Let(
        { user: Select(["data"], Get(Match(Index("user_by_id"), Var("ID")))) },
        Do(
          If(
            Equals(CurrentIdentity(), Select(["user_id"], Var("user"))),
            true,
            Abort("permission denied")
          ),
          Var("user")
        )
      )
    )
  ),
  role: Role("usersCRUD")
})
```

*   Per our API spec, user’s `user_id` must match the `sub` claim. So we test it with `If(Equals(...)...)` and `Abort()` if `false`.

And do the same for _UpdateUser_:

```
CreateFunction({
  name: 'UpdateUser',
  body: Query(
    Lambda(
      ["ID", "data"],
      Let(
        {
          match: Match(Index("user_by_id"), Var("ID")),
          user: Get(Var("match"))
        },
        Do(
          If(
            Equals(CurrentIdentity(), Select(["data", "user_id"], Var("user"))),
            true,
            Abort("permission denied")
          ),
          Let(
            {
              ref: Select(["ref"], Var("user"), null),
              email: Select(
                ["email"],
                Var("data"),
                Select(["data", "email"], Var("user"), null)
              ),
              profile: Select(
                ["profile"],
                Var("data"),
                Select(["data", "profile"], Var("user"), null)
              ),
              updateData: Merge(
                { email: Var("email") },
                { profile: Var("profile") }
              )
            },
            Update(Var("ref"), { data: Var("updateData") })
          ),
          Select(["data"], Get(Var("match")))
        )
      )
    )
  ),
  role: Role("usersCRUD")
})
```

*   Similar to _GetUser_, the UDF allows updates only if the JWT `sub` is the same as the User being updated.
*   To enforce our REST APIs Users schema, any field other than `email` or `profile` will be ignored if present in the request body. So we use `Merge({ email: ...}, { profile: ...})` to do this.

Finally, update the _APIsRole_ role so that it can execute the 2 new UDFs above:

```
Update(Role('APIsRole'),
  {
    privileges: [
      { resource: Function("CreateUser"), actions: { call: true } },
      { resource: Function("GetUser"), actions: { call: true } },
      { resource: Function("UpdateUser"), actions: { call: true } }
    ]
  }
)
```

As you’ve already realized, these UDFs got pretty elaborate as Fauna lets us do everything we need including validating the JWT for embedded permissions and formatting our specific API responses. This allowed us to stick to our goal of pushing the bulk of logic down to Fauna while keeping the implementation at the edge pretty lightweight.

Now we’ll just have to update `/src/index.js` to handle `GET` and `PUT` by calling the 2 new UDFs. While we’re at it, let's also finish the whole implementation by also handling `GET /`, which should return our Single Page Application (SPA). With this, our service is serving both REST APIs and the SPA.

```js
import {
  badRequest, getFaunaError,
  resolveBackend, formatFaunaCallFunction
} from './utils.js';

addEventListener('fetch', event => event.respondWith(handleRequest(event)));

async function handleRequest(event) {
  const req = event.request;

  // Allows the edge to respond to CORS
  if (req.method === "OPTIONS" && req.headers.has("Origin") && (
    req.headers.has("access-control-request-headers") ||
    req.headers.has("access-control-request-method"))
  ) {
    return new Response(null, {
      status: 204,
      headers: {
        "access-control-allow-origin": "*",
        "access-control-allow-methods": "GET,HEAD,POST,PUT,OPTIONS",
        "access-control-allow-headers": req.headers.get('access-control-request-headers') || '',
        "access-control-max-age": 86400,
      }
    });
  }

  const VALID_METHODS = ["GET", "POST", "PUT"];
  if (!VALID_METHODS.includes(req.method)) {
    const response = new Response("This method is not allowed", {
      status: 405
    });
    return response;
  }

  const method = req.method;
  const url = new URL(event.request.url);
  const pathname = url.pathname;

  // GET "/", "/js", "/css", "/favicon.ico" returns static contents from Object store bucket
  if (method == "GET" &&
    ["", "js", "css", "favicon.ico"].includes(pathname.split("/")[1])) {

    // Below is an example. Update and point to your own bucket 
    const SPA_HOST = 'example-bucket.s3-website-us-west-2.amazonaws.com';
    const SPA_BACKEND = 's3staticwebsite';

    const s3StaticWebsite = new Request(`http://${SPA_HOST}${pathname}`, {
      method: "GET"
    });

    const s3res = await fetch(s3StaticWebsite, { backend: SPA_BACKEND });

    let headers = new Headers();
    const resource = pathname.split("/")[1];
    if (resource == 'js')
      headers.set('Content-Type', 'application/javascript');
    else if (resource == 'css')
      headers.set('Content-Type', 'text/css; charset=utf-8');
    else if (resource == 'favicon.ico')
      headers.set('Content-Type', 'image/x-icon');
    else
      headers.set('Content-Type', 'text/html; charset=utf-8');

    return new Response(await s3res.text(), {
      status: 200,
      headers
    });
  }

  // POST /users
  if (method == "POST" && pathname == "/users") {
    try {
      const reqBody = await req.json();
      return await callUDF(req, () => {
        return formatFaunaCallFunction('CreateUser', null, reqBody);
      });
    } catch {
      return badRequest();
    }
  }

  if (pathname.match(`\/users\/[^\/]+(\/)?$`)) {

    const userId = decodeURI(pathname.split('/')[2]);

    // GET /users/{id}
    if (method == "GET") {
      return await callUDF(req, () => {
        return formatFaunaCallFunction('GetUser', userId, null);
      });
    }

    // PUT /users/{id}
    if (method == "PUT") {
      try {
        const reqBody = await req.json();
        return await callUDF(req, () => {
          return formatFaunaCallFunction('UpdateUser', userId, reqBody);
        });
      } catch {
        return badRequest();
      }
    }
  }

  return new Response("The page you requested could not be found", {
    status: 404
  });
};

async function callUDF(request, formatHandler) {
  try {
    const { backend, backendUrl, bearerToken } = resolveBackend(request);

    // formatHandler translates REST request into FQL "Call(Function('name'))" equivalent
    const body = formatHandler();

    const headers = new Headers({
      "Authorization": `Bearer ${bearerToken}`,
      "Content-Type": "application/json"
    });

    const faunaRest = new Request(backendUrl, {
      method: "POST",
      headers: headers,
      body: JSON.stringify(body)
    });

    const res = await fetch(faunaRest, { backend: backend });

    let response = await res.json();

    // If FQL throws an error, return error
    const faunaErrors = getFaunaError(response);
    if (faunaErrors) {
      return new Response(
        faunaErrors.description, {
        headers: { 
          "content-type": "application/json;charset=UTF-8",
          "Access-Control-Allow-Origin": "*"
        },          
        status: faunaErrors.status
      });
    } else {
      return new Response(
        JSON.stringify(response.resource), {
        headers: { 
          "content-type": "application/json;charset=UTF-8",
          "Access-Control-Allow-Origin": "*"
        },
        status: 200
      });
    }
  } catch (e) {
    console.log(`${e}`);
    return new Response(`${e}`, { status: 500 });
  }
}
````

*   In the above code, notice that Compute@Edge is performing a fetch from the object store that hosts the SPA, so the domain of SPA\_HOST must also be whitelisted as a “host” in the Compute@Edge service’s Origins > Hosts settings. (We did this step with the Fauna EU domain endpoint)

Now build with the same command used previously:

```
fastly compute build
```

And then deploy:

```
fastly compute deploy
```

After the command completes, wait just a few seconds (about 30 seconds) for the service to fully propagate. Then you should be able to test:

Example GET /users/{id} test with curl

```
curl -H "Authorization: Bearer eyJhb..."

https://frequently-faithful-mouse.edgecompute.app/users/uf7TdWaOKyWvLahkO2oxJi0AWBdVg24K@clients
```

*   Replace `uf7TdWaOKyWvLahkO2oxJi0AWBdVg24K@clients` with your own user\_id

_Response_:

```
{
  "user_id": "uf7TdWaOKyWvLahkO2oxJi0AWBdVg24K@clients",
  "email": "rainer.theo@mailinator.com"
  "profile": {
    "name": "Rainer Theo",
    "country": "DE",
    "age": "23"
  } 
}
```

Example PUT /users/{id} test with curl

```
curl -X PUT  -H "Content-Type: application/json" \
     -H "Authorization: Bearer eyJhb..."  \
     -d '{"email":"rainer.theo@mailinator.com", "profile": { "name": "Rainer Theo", "country": "DE", "age": 30 } }' \
     https://frequently-faithful-mouse.edgecompute.app/users/uf7TdWaOKyWvLahkO2oxJi0AWBdVg24K@clients
```

*   Replace `uf7TdWaOKyWvLahkO2oxJi0AWBdVg24K@clients` with your own user_id

_Response_:

```
{
  "user_id": "uf7TdWaOKyWvLahkO2oxJi0AWBdVg24K@clients",
  "email": "rainer.theo@mailinator.com"
  "profile": {
    "name": "Rainer Theo",
    "country": "DE",
    "age": 30
  } 
}
```

If you configured `SPA_HOST` and `SPA_BACKEND` (and have uploaded a SPA to the `SPA_HOST`), you’ll be able to see it by opening a browser to / at the Compute@Edge domain, e.g. `https://frequently-faithful-mouse.edgecompute.app/`.

## Sample App

We’ve built a [sample app](https://github.com/fauna-labs/vue-fauna-edge-api) to demonstrate how the solution comes together. One of the beauties of using Auth0 is we don’t even have to build a login and signup page as it is provided out-of-the-box.

![](https://images.ctfassets.net/23aumh6u8s0i/4s8CdrEaim6nLoZjOD6zTm/0d7180da7e3debb8168ef769e546f72d/Uploaded_from_Building_an_edge_API_gateway_with_Fauna_and_securing_it_with_Auth0)

## Going to the Next Level

Being able to run logic at the edge provides you full control over the routing of your application. In addition to what’s already been covered in this tutorial, you can also:

*   Stitch together multiple backends.
*   Route to other resources (other than Fauna) and serve them under the same domain.
*   Etc.

Take a look at some [examples](https://developer.fastly.com/solutions/examples/javascript/) to get a feel of what you can achieve.

An advanced capability involves Geo-IP’ing the requests at the edge, allowing us to serve region specific traffic based on the location of the client. Let’s go back to this picture:

![Fastly Fauna GDPR.drawio](https://images.ctfassets.net/23aumh6u8s0i/72F9duizmcoHuJ7PB4RtTW/77697bf18ec0779b515f0a1ae7c8fee2/Uploaded_from_Building_an_edge_API_gateway_with_Fauna_and_securing_it_with_Auth0)

Let’s assume that our requirements are that EU users' data must be stored in the EU. As for users coming from anywhere else, it doesn’t matter. We’re also going to serve a different version of the SPA to EU users — one that has additional consent screening as well as EU-specific content.

A high-level walkthrough of how we’ll enhance our setup is as follows:

Use [event.client.geo](https://developer.fastly.com/solutions/examples/geo-ip-api-at-the-edge) to detect if requests come from the EU or not. As an example, within `handleRequest()` of `index.js`, we can include the following code to logically send an EU or non-EU version of the SPA to users:

```js
const clientGeo = event.client.geo;
if (clientGeo.continent == 'EU') {
    SPA_HOST = 'eu-version.s3-website-eu-central-1.amazonaws.com';
    SPA_BACKEND = 's3staticwebsite-eu';
} else {
    SPA_HOST = 'base-version.s3-website-us-east-1.amazonaws.com';
    SPA_BACKEND = 's3staticwebsite';
}
````

Then, create a US Region Group (or Classic Region Group) Fauna database and replicate everything we’ve setup in the tutorial. This includes setting up the AccessProvider in Fauna and corresponding API in Auth0. Thus, a user logging in to the EU website gets a different `aud` claim than a user logging in from the non-EU website. We then update `resolveBackend()` in `utils.js` to route API requests to the EU Region Group or US Region Group as follows:

```js
export function resolveBackend(request) {
  try {
    const bearerToken = request.headers.get('Authorization').split('Bearer ')[1];

    const decodedPayload = JSON.parse(
      Buffer.from(jwtPayload, 'base64').toString('utf-8')
    );
    const aud = decodedPayload['aud'];

    // we can store and lookup our audiences in the edge dictionary
    const exampleDictionary = new Dictionary("example_dictionary");
    const audEU = exampleDictionary.get("aud_eu");

    if (aud == audEU) {
      backend = 'db_eu_fauna_com';
      backendUrl = 'https://db.eu.fauna.com';    
    } else {
      backend = 'db_us_fauna_com';
      backendUrl = 'https://db.us.fauna.com';        
    }

    return { backend, backendUrl, bearerToken };
  } catch (e) {
    console.log(`${e}`);
    throw e;
  }
}
```

And now, when the edge detects that the request is coming from the EU region, the EU version of the website is shown. And users are automatically created in the EU Region Group. Conversely, requests coming from outside the EU will be served the international version of the site and users created in the US/Classic Region Group.

## Wrapping Up

Edge computing enables developers to deploy application logic closer to the end-user for reduced latency. But if the database is itself a performance bottleneck due to its distance from the user, we haven’t solved the latency problem. Fauna is a globally distributed database, making it a great fit for developers to pair with Fastly’s Compute@Edge – keeping both compute and the data close to the end-user, and in turn keeping global latency low.

Auth0 allows developers to focus on building applications and worry less about security and building their own authentication and identity solutions. And Fauna natively integrates with Auth0, further enhancing our promise to drive developer productivity. Finally, the architecture’s completely serverless footprint allows developers to focus more on building and less on managing infrastructure.