Faunadb: The Modern Database for Developers
Tags: database, fauna, javascript
Published: 2023-08-14 08:42:27

Faunadb is a multi-paradigm relational database with a built-in document data model. Faunadb gives developers the benefit of working with relational and document databases inside a cloud API that can be accessed from almost any platform. Faunadb has its query language called FQL (Fauna Query Language) an expression-oriented query language that enables complex, precise manipulation and retrieval of documents. All FQL queries are transactional ensuring data integrity. FaunaDB is Multi-regional and scales up horizontally using active-active clusters while incorporating most modern application security features.

In today's post, we will take a glance at the following while we uncover what Faunadb is;

- How it works
- Create an account & database
- Setup a local development
- Running FQL queries


## How Faunadb works?

### Documents

Faunadb stores your data as JSON-like objects. This approach is similar to what you'd observe in MongoDB, Firestore, and other document-oriented databases. All documents have a timestamp and a unique Ref. A Document Ref is comprised of the name of the collection and a number that is inside a string. All documents are immutable by default and updates create new documents each with its timestamp allowing for time-lapse queries.

### Collection

Collections just serve as a way to group related documents and they are similar to tables in SQL. Multiple schemaless documents can be stored inside a collection and when a collection is deleted, associated documents become inaccessible and get deleted asynchronously.

### Reference

A reference is a compound value that serves as a unique ID for any document in a collection, A reference is comprised of a reference to the collection containing the document and a document ID which is a number. two documents in a database can share the same reference.

### Indexes

Indexes allow for organization and easy retrieval of documents by attributes of the data. An index is just a lookup table that speeds up finding documents,  You can query an index to find the documents inside as opposed to reading all documents inside a collection just to select a few.

### Functions

The FQL provides built-in functions which can be used to run queries. We can compose multiple built-in functions to create User-defined functions (or UDFs) UDFs are used to combine frequently used functions into queries that can be stored and executed repeatedly.

## Create an account

To create a Faunadb account you need to head over to the [Fauna website](https://fauna.com). Click on Start for free then select Github as your auth provider, and authorize Faunadb from GitHub. If everything went well, your account should successfully be created for you and you should be redirected to your Faunadb dashboard.

### Setup a database

Now you've successfully created an account, on your dashboard click on the create database button, next we need to give our database a name, select a region for your data, and select use `demo-data` to ensure that the database is created with some demo data for us. Go ahead and click on create and you should be redirected to the overview page for your database if everything went according to plan.

## Project Setup

Open up an empty folder in your favorite editor and let's set up a NodeJS app with Faunadb. Run the following command to spin up a new node project

```bash
$ npm init --y
```

Next, we need to install some basic extensions to help get the server started, 
first, let's install Express to help create a basic server for us.

```bash
$ npm i express
```
run the following command to install the Faunadb and dotenv extension.

```bash
$ npm i faunadb dotenv
```

Now, we've successfully installed our app dependencies, go back to your Faunadb dashboard, and ensure you click on the newly created database. Click on the security tab and click on Create a new database key. Give the key a name and ensure that you set the role for the key to be server and proceed. If everything went according to plan you should see an API key generated for you, copy this key and store it in a dotenv file in your project directory.

## Running FQL queries

Create a new javascript file with the name `app.js`. We will write and export the functions for interacting with Faunadb inside this module. The first thing we need to do is to create a collection so let's write the logic for that.

```javascript
const { query, Client } = require('faunadb');
require('dotenv').config();

const client = new Client({
  secret: process.env.SECRETE_KEY,
});

//Create a collection
const createCollection = async (name) => {
  try {
    const collection = await client.query(
      query.CreateCollection({ name })
    );
    return [null, collection]
  } catch (error) {
    return [error, collection]
  }
}

module.exports = {
  createCollection,
}
```

We imported the `query` object alongside the `Client`. The `query` object houses the methods we need for interacting with our Faunadb database. We then create a new instance of the `Client` and we need to authenticate our client using the secret key we created earlier. Now we have a client let's go ahead and start making queries. We have a function `createCollection`. This function accepts a single parameter `name` which will serve as the name of the collection we are creating.  There's a `try/catch` block to wrap up the process of creating the collection and handling our errors gracefully. This will be our pattern for other functions we will create. Inside the `try` block we call await `client.query` and this is how you make a query using FQL.

This function accepts a query as its argument. To create a collection, we call `query.CreateCollection` and pass in an object with a name property as an argument to it. We store the result of the query which is the newly created collection inside a variable `collection`. We return an array with null as the first value and the `collection` as the second value. This will also be a pattern in this codebase, all helper functions must return an array object with the first value representing an error object and the second value being the return value from the function. 

Inside the `catch` block we return an array too, matching the pattern for returning an array from helper functions, this time the first value is the error object thrown while the second argument is null. Then we use `module.export` to export the `createCollection` function. Now let's create a basic express server inside a new file `index.js`

```javascript
// index.js

const express = require('express');
const { createCollection } = require('./app');

const app = express();
app.use(express.json());

app.post('/collection', async (req, res) => {
  const {name} = req.body;
  try {
    const [error, collection] = await createCollection(name);
    if (error) throw error
    if (collection) res.status(200).json(collection)
  } catch(error) {
    return res.status(400).json(error)
  }
})

app.listen(3000, () => console.log('App running on PORT 3000'));

```

Inside the `index.js` file we import express then we import the `createCollection` function from `app.js`. We create a new express app and use the JSON parser on express as an app middleware. Next, we register a route and a handler for a post request. The route is `/collection` and inside the handler function, we destructure the name from the request body, and inside the `try` block we call await `createCollection` and pass in the name. We destructure the `error` and the `collection`. If there is an `error` then we just throw the error else if the collection is created successfully we send it back with the response. 

In the `catch` block we just set the status of the response to `400` then we send the error back with the response then we listen on port 3000 for a connection. If you run this app with nodemon;

```bash
$ nodemon i index.js
``` 
If everything went well you should see the message `App running on PORT 3000` in your console. Let's make a query to the endpoint to test it out.

```curl
POST http://localhost:3000/collection
Content-Type: application/json

{
    "name": "todos"
}
```

If you make this request you should get a response that looks like this;

```json
{
  "ref": {
    "@ref": {
      "id": "todo",
      "collection": {
        "@ref": {
          "id": "collections"
        }
      }
    }
  },
  "ts": 1691589603965000,
  "history_days": 0,
  "name": "todo"
}
```

Now we have a collection let's go ahead and add some data to it.

```javascript
// app.js continued

const createTodo = async (note) => {
  try {
    const todo = await client.query(
      query.Create(
        query.Collection('Todos'),
        { data: { status: 'PENDING', note } }
      )
    );
    return [null, todo];
  } catch (error) {
    return [error, null];
  }
}

module.exports = {
  createCollection,
  createTodo
}
```
The snippet above handles the process of creating a new Todo object. We have a function `createTodo` which accepts a string `note` as its argument. Inside a `try` block we call `client.query` and pass in a query to it. To create a new document you need to call `query.Create` then you pass a ref to the collection you want the document to belong to. The second argument the `Create` method accepts is a data object with a `data` property. The value for this `data` property is the data we want to store in the document. Our data will have a `status` property which will be `PENDING` and a note property which will serve as the title of the todo. Let's set up the handler for it.

```javascript
//index.js continued

const {
  createCollection,
  createTodo,
} = require("/.app.js");


app.post('/todo', async (req, res) => {
  const {note} = req.body;
  try {
    const [error, todo] = await createTodo(note);
    if (error) throw error
    if (todo) res.status(200).json(todo)
  } catch(error) {
    return res.status(400).json(error)
  }
})
```
We register a post-request handler on our app. The route is `/todo` and inside the handler, we destructure the note from the request body. Inside the `try` block we create a new todo and send it back with the response otherwise if there's an error we send that error back with a status of 400. Now let's test this endpoint.

```curl
POST http://localhost:3000/todo
Content-Type: application/json

{
    "note": "Cleaning"
}
```

The response from the server should look like this;

```json
{
  "ref": {
    "@ref": {
      "id": "372695699207225424",
      "collection": {
        "@ref": {
          "id": "todos",
          "collection": {
            "@ref": {
              "id": "collections"
            }
          }
        }
      }
    }
  },
  "ts": 1691689166285000,
  "data": {
    "status": "PENDING",
    "note": "Cycling"
  }
}
```
Go ahead and create a couple of more todos for fun, now we need to set up an index. Indexes are central to FQL and they are going to be how we will retrieve data from FQL, let's set up an index to retrieve all the pending todos.

```javascript
// app.js continued

// CREATE INDEX
const createIndex = async (
  name,
  collection,
  field
) => {
  try {
    let index;
    if (field) {
      index = await client.query(
        query.CreateIndex({
          name,
          source: query.Collection(collection),
          terms: [{ field }],
        })
      );
    } else {
      index = await client.query(
        query.CreateIndex({
          name,
          source: query.Collection(collection),
        })
      );
    }
    return [null, index];
  } catch (error) {
    console.log(error)
    return [error, null];
  }
}



module.exports = {
  createCollection,
  createTodo,
  createIndex,
}
```

First, we define the `createIndex` function. The function takes three arguments and returns an array with two elements. Inside the try block, we declare a variable called `index`. This variable will store the created index. Then we check if the field argument is not undefined. If it is not undefined, the function creates an index on the specified field. Otherwise, the function creates an index on the entire collection. We use the `client.query()` method to create the index. The `query.CreateIndex()` method creates a new index. The name argument specifies the name of the index. The source argument specifies the collection that the index will be created on. The terms argument specifies the fields that the index will be created on. Then we export the function out of this module. Let's set up a handler for this.

```javascript
// index.js continued

const {
  createIndex
} = require('./app');

app.post('/index', async (req, res) => {
  const {name,collection,field} = req.body;
  try {
    const [error, index] = await createIndex(
      name,
      collection,
      field ?? null 
    );
    if (error) throw error
    if (index) res.status(200).json(index)
  } catch(error) {
    return res.status(400).json(error)
  }
});

```

I have attached a handler function to the `/index` route, that will be executed when a POST request is made to that route. The route handler function first parses the request body and extracts the `name`, `collection`, and `field` parameters. The name parameter specifies the name of the index to create. The collection parameter specifies the name of the collection that the index will be created on. The field parameter specifies the field that the index will be created on. If this parameter is omitted, the index will be created on the entire collection.

The function then calls the `createIndex` function to create the index. The `createIndex` function returns an array with two elements: the first element is the error message, and the second element is the index. If the createIndex function returns an error message, the handler function throws an error. The error will be sent back to the client as a 400 Bad Request response. If the createIndex function returns the index, the handler function sends the index back to the client as a 200 OK response. Let's test this endpoint.

```curl
POST http://localhost:3000/index
Content-Type: application/json

{
  "name": "todos_by_status",
  "collection": "todos",
  "field": ["data", "status"]
}
```

 The response should look like this;

```json
{
  "ref": {
    "@ref": {
      "id": "todos_by_status",
      "collection": {
        "@ref": {
          "id": "indexes"
        }
      }
    }
  },
  "ts": 1691691134550000,
  "active": true,
  "serialized": true,
  "name": "todos_by_status",
  "source": {
    "@ref": {
      "id": "todos",
      "collection": {
        "@ref": {
          "id": "collections"
        }
      }
    }
  },
  "partitions": 8
}
```

Now let's set up a function to retrieve all the pending todos;

```javascript 
// app.js continued

const getTodoByIndex = async (
  index,
  value
) => {
  try {
    const todos = await client.query(
      query.Map(
        query.Paginate(
          query.Match(
            query.Index(index),
            value
          )
        ),
        query.Lambda(x => query.Get(x))
      )
    );
    return [null, todos]
  } catch (error) {
    return [error, null]
  }
}

module.exports = {
  createCollection,
  createTodo,
  createIndex,
  getTodoByIndex,
}
```

The snippet above first defines a function called `getTodoByIndex`. The function takes two arguments: the `index` argument specifies the name of the index, and the `value` argument specifies the value that the index will be matched against. The function then uses the `client.query()` method to execute the query. The client variable is a reference to the FaunaDB client. The `query.Map()` method creates a new map. The `query.Paginate()` method paginates the results of the query.

The `query.Match()` method matches the documents in the collection that match the specified index and value. The `query.Lambda()` method creates a lambda function. The lambda function will be executed for each document that is returned by the query. The `query.Get()` method gets the document from the database. The function returns an array with two elements: the first element is the error message, and the second element is the array of documents that were returned by the query. Let's set up a handler function to retrieve the pending todo.

```javascript
// index.js continued
const {
  getTodoByIndex
} = require("./app");

app.get('/todos/index',  async (req, res) => {
  const {index, value} = req.body
  try {
    const [error, todo] = await getTodoByIndex(
      index,
      value
    );
    if (error) throw error
    if (todo) res.status(200).json(todo)
  } catch(error) {
    return res.status(400).json(error)
  }
})

```

We have attached a route handler function to the `/todos/index` route, that will be executed when a GET request is made to that route. The function first parses the request body and extracts the `index` and `value` parameters. The `index` parameter specifies the name of the index, and the `value` parameter specifies the value that the index will be matched against. The function then calls the `getTodoByIndex` function to retrieve the todos. 
If the getTodoByIndex function returns an error message, the handler function throws an error. The error will be sent back to the client as a 400 Bad Request response. If the getTodoByIndex function returns an array of documents, the handler function sends the array of documents back to the client as a 200 OK response. Let's test this endpoint.

```curl
GET http://localhost:3000/todos/index
Content-Type: application/json

{
  "index": "todos_by_status",
  "value": "PENDING"
}
```
We should get back an array of todos, let's create a handler function to retrieve a single todo;

```javascript
// app.js continued

const getTodo = async (id) => {
  try {
    const todo = await client.query(query.Get(
      query.Ref(
        query.Collection("todos"),
        id
      )
    ));
    return [null, todo];
  } catch (error) {
    return [error, null];
  }
}


module.exports = {
  createCollection,
  createTodo,
  createIndex,
  getTodoByIndex,
  getTodo,
}
```
We have defined a function called `getTodo` that gets a `todo` document from the todos collection. The function takes one argument: the `id` argument which specifies the ID of the document to get. The function first uses the `client.query()` method to execute the query. The `query.Get()` method gets the document from the database. The `query.Ref()` method creates a reference to the document.

The `query.Collection()` method specifies the collection that the document is in. The `id` argument specifies the ID of the document. The function returns an array with two elements: the first element is the error message, and the second element is the document that was returned by the query. Let's set up a handler function for retrieving a todo

```javascript
// index.js continued
const {getTodo} = require('./app');

app.get('/todo/:id', async (req, res) => {
  const {id} = req.params;
  try {
    const [error, todo] = await getTodo(id);
    if (error) throw error
    if (todo) res.status(200).json(todo)
  } catch(error) {
    return res.status(400).json(error)
  }
})
```

We have a route handler function that gets a todo document from the todos collection by ID. The function is attached to the `/todo/:id` route, so it will be executed when a GET request is made to that route. The function first parses the request parameters and extracts the id parameter. The `id` parameter specifies the ID of the document to get. The function then calls the `getTodo` function to get the todo. If the getTodo function returns an error message, the handler function throws an error. The error will be sent back to the client as a 400 Bad Request response. If the getTodo function returns a document, the handler function sends the document back to the client as a 200 OK response. Finally, let's set up a helper function to update a todo.

```javascript
// * UPDATE TODO
const updateTodo = async (id, status) => {
  try {
    const todo = await client.query(
      query.Update(
        query.Ref(query.Collection("Todos"), id),
        { data: { status: status } },
      )
    )
    return [null, todo];
  } catch (error) {
    return [error, null]
  }
}

module.exports = {
  createCollection,
  createTodo,
  createIndex,
  getTodo,
  getTodoByIndex,
  updateTodo
}

```

I have created a function called `updateTodo` that updates a todo document in the todos collection. The function takes two arguments: the `id` argument specifies the ID of the document to update, and the status argument specifies the new status of the document. The `query.Update()` method updates the document. The `query.Ref()` method creates a reference to the document. The `query.Collection()` method specifies the collection that the document is in. The `id` argument specifies the ID of the document. The data object specifies the data that will be updated in the document. In this case, the status property is being updated. The function returns an array with two elements: the first element is the error message, and the second element is the document that was updated. Let's set up a route handler function for this;

```javascript
// index.js continued

const {updateTodos} = require('./app');

app.patch('/todo/:id',  async (req, res) => {
  const {id} = req.params;
  const {status} = req.body;
  try {
    const [error, todo] = await updateTodo(id, status);
    if (error) throw error
    if (todo) res.status(200).json(todo)
  } catch(error) {
    return res.status(400).json(error)
  }
})
```
We have a created route handler function that updates the status of a todo document in the todos collection. The function is attached to the `/todo/:id` route, so it will be executed when a PATCH request is made to that route. The function first parses the request parameters and extracts the id and status parameters. The `id` parameter specifies the ID of the document to update, and the status parameter specifies the new status of the document. The function then calls the `updateTodo` function to update the todo.

If the `updateTodo` function returns an error message, the handler function throws an error. The error will be sent back to the client as a `400` Bad Request response. If the updateTodo function returns a document, the handler function sends the document back to the client as a `200` OK response.

I'm going to stop here and allow you to figure out how to retrieve all the documents in a collection and also how to delete a document from a collection. There are lots of things we skipped because of the time factor, Faunadb has a graphQL API for interacting with your data. We are not limited to using Faunadb on the backend, we can also use it in frontend apps like a react or svelte project. We just barely touched Lambda functions in FQL, we just brushed the surface of FQL and each of these concepts requires a separate post on its own. Leave your thoughts in the comment section below about Faunadb, FQL and the next concept we should cover.