How to build serverless with Netlify and FaunaDB

26 November 2020
12 min read

In What is Cloud Computing we discussed why the Serverless Computing model has become a popular choice for developers, enabling them to quickly build and deploy their applications without having to concern themselves with Infrastructure complexities. In this post, we'll provide an example of how we leverage serverless technology to enable us to build features on this website!

We'll walk you through our process of using:

What will we be building

For the most part, we will be building a typical User Registration work flow, enabling a user to create an account, confirm their email address and add & edit their User Profile details. Our final solution actually makes use of NU_ID Trustless Authentication to manage the authentication, we will provide details on that implementation in future articles.

Gridsome, Netlify functions and FaunaDB

Our solution make use of Gridsome static website generator, and I have previously blogged about Getting started with Gridsome Static website generation and it may be worth reading that article if you are unfamiliar with Gridsome or the Jamstack and Netlify in general. For the remainder of this article I will assume you have some familiarity with Gridsome and Netlify.

If you would like more information on how to use Gridsome and Netlify functions in general I would recommend reading Gridsome – How to use netlify functions because the article provides a background on how to configure your local development environment in order to get setup to start developing making use of Netlify Functions and the Netlify CLI

I am going to assume some level of familiarity of Gridsome, Netlify CLI and a Fauna Account in order to follow along with this guide.

Sign Up with Fauna for free
  • Fauna core (FQL & GraphQL)
  • Built-in security (ABAC)
  • Temporality (24 hour retention)
  • Multi-factor authentication
  • Real-time document streams
  • Standard regions

To set up a Fauna database to store our User data, you will need to set up an account and get the API Key, we'll be using this to scaffold our Database and Collections. To create an account https://dashboard.fauna.com/accounts/register

Register environment variables

The first thing we will need to do is ensure both our Development server and production server have all the environment variables we need.

You will also need to create an Access Key to your Fauna Database, read the security section of the Fauna

Once you have your key you can create a local env.development in the root project directory and add a new environment variable which will store your Key you generate in your Fauna Dashboard. The standard name to use is FAUNADB_SERVER_SECRET

FAUNADB_SERVER_SECRET=<your key from fauna>

You can also register this key on your Netlify Deploy Dashboard in Build environment variables

Create a Netlify function

If you are not familiar with Netlify Functions and how to start developing them, then please take the time to read How To Build A Netlify Function
as we walk you through the process and introduce all the tools you need to start developing Netlify Functions.

In our case we are going to creat a functions folder in the root of our project directory and in this folder we will create another folder name users, because the functionality we will be building here is a typical User Management system, enabling users create and update their information, which we will be storing in Fauna.

mkdir functions && cd functions
mkdir users && cd users

We are also going to be developing a function that will be making use of the FaunaDB client library, so we will need to install the npm package to folder. We will need to initialise our project

npm init -y

We can now install the faunadb package

npm i faunadb

Check out How To Use Package Management In JavaScript Projects to learn more about package management in JavaScript projects.

Get started with Netlify CLI

The Netlify CLI has templates available that can simply these steps. It's well taking the time to examine the templates offered.

Update Netlify Configuration file

In How To Build A Netlify Function we briefly introduced the Netlify Configuration file netlify.toml, we will now need to add an additional section to our configuration file. We need to include an additional Netlify Build plugin because we are developing a function that going to install its own dependencies. In our case, we don't want to include the dependency to our top level package.json i.e the Gridsome project packcage.json, primarily because at some point in the future we will want to move our functions to a separate repository and possibly deploy them to a different website. We want to develop the using the microservice that each service is a self-contained unit.

We need to install the @netlify/plugin-functions-install-core build plugin, to do this we just add the following to our netlify.toml file.

[[plugins]]
     package =  "@netlify/plugin-functions-install-core"

Create Fauna Collection

We will now start to create the elements of our function, and the first step in this direction is to create a script that will help to create the Database Collection we'll need for our function. In this function, we are going to assume that the Fauna Database has been created. This can be a fairly safe assumption, because we would have had to create the database to register the security key we're using to access the Fauna.

In order to keep things simple from this tutorial perspective, we only need to ensure that the collection can be created from the terminal.

FaunaDB is a rare breed in the world of databases as it allows you to model and query your data using different paradigms:

  • Relational
  • Documents (schemaless)
  • Temporal
  • Graph-like

The first file we're going to create is a plain Javascript file which will contain some Global variables we are going use across a number of files in our function. We'll create a collection.js in our function folder. Essentially this file contains variable for the name of our collection and the name of its associated index.

module.exports.name = 'users'
module.exports.index = 'all_users'

The next file we'll create is the create-schema.js which will be the file that will create our initial collection.

You'll notice that in this file we import our collection.js file which contains our two variable values we set previously. We also import the Fauna dependency we installed, and we also access the Secret we configured.

Once we have those details with instantiate the Fauna DB client and create a Collection and associated Index.

#!/usr/bin/env node
/* use with `netlify dev:exec <path-to-this-file>` */
const process = require('process')
let collection = require('./collection')

const { query, Client } = require('faunadb')

const createSchema = function () {
    if (!process.env.FAUNADB_SERVER_SECRET) {
        console.log('Fauna Secret Environment variable does not exist.')
        console.log('Database cannot be created.')
    }

    console.log(`A collection with the name ${collection.name} will be created`)

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

    return client
        .query(query.CreateCollection({ name: collection.name }))
        .then(() => {
            console.log(`created ${collection.name} collection`)
            return client.query(
                query.CreateIndex({
                    name: collection.index,
                    source: query.Collection(collection.name),
                    active: true,
                })
            )
        })
        .catch((error) => {
            if (
                error.requestResult.statusCode === 400 &&
                error.message === 'instance not unique'
            ) {
                console.log(`Collection: ${collection.name} already exists`)
            }
            throw error
        })
}

createSchema()

We can now run this file to create our collection. Using the terminal window we can use the Netlify CLI to execute our script.

 netlify dev:exec functions/users/create-schema.js 

Create Schema with Fauna

We will see the confirmation message that the Collection has been created. We can also log in to our Fauna Dashboard and view our GeekIam database we'll see our new collection exists.

Fauna Collection Created

Create User Script

We create the first lambda for a basic CRUD API we're developing, this function will be responsible for creating a record in our Users collection. You'll notice we import the Collection and Fauna Client, then we simply save whichever JSON is passed to the function to Fauna.

The function saves the data to the database, then simply returns the saved record back to client.

const process = require('process')

const { query, Client } = require('faunadb')
const collection = require('./collection')

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

exports.handler = async function (event) {
    const data = JSON.parse(event.body)

    const item = {
        data,
    }

    return client
        .query(query.Create(query.Collection(collection.name), item))
        .then((response) => {
            return {
                statusCode: 200,
                body: JSON.stringify(response),
            }
        })
        .catch((error) => {
            console.log('error', error)
            /* Error! return the error with statusCode 400 */
            return {
                statusCode: 400,
                body: JSON.stringify(error),
            }
        })
}

Read User Script

We'll create the second component of function, which is the Read function. It follows a similar pattern as the previous function. We pass in the ID of the record we want we simply query Fauna to get the details.

const process = require('process')

const { query, Client } = require('faunadb')
const collection = require('./collection')
const client = new Client({
    secret: process.env.FAUNADB_SERVER_SECRET,
})

exports.handler = async function (event) {
    const { id } = event
    console.log(`Function 'read' invoked. Read id: ${id}`)
    return client
        .query(query.Get(query.Ref(query.Collection(collection.name), id)))
        .then((response) => {
            console.log('success', response)
            return {
                statusCode: 200,
                body: JSON.stringify(response),
            }
        })
        .catch((error) => {
            console.log('error', error)
            return {
                statusCode: 400,
                body: JSON.stringify(error),
            }
        })
}

Code on Github
For the sake of brevity we have not included all the code in this article. If you would like to see the full code listing check out full code in our Github repo

Gateway

The last lambda we'll create will be our Controller gateway type of lambda. This will easily allow us to implement more stringent security later, but for now what it enables us to do is point our applications at one route. i.e. /users and the lambda itself will determine what route it needs to enable by inspecting the HttpMethod call.

const createRoute = require('./create')
const deleteRoute = require('./delete')
const readRoute = require('./read')
const readAllRoute = require('./read-all')
const updateRoute = require('./update')
const collection = require('./collection')

exports.handler = async function (event, context) {
    const path = event.path.replace(/\.netlify\/functions\/[^/]+/, '')
    const segments = path.split('/').filter(Boolean)

    switch (event.httpMethod) {
        case 'GET':
            if (segments.length === 0) {
                return readAllRoute.handler(event, context)
            }
            if (segments.length === 1) {
                const [id] = segments
                event.id = id
                return readRoute.handler(event, context)
            }
            return {
                statusCode: 500,
                body: `too many segments in GET request, must be either /.netlify/functions/${collection.name} or /.netlify/functions/${collection.name}/123456`,
            }

        case 'POST':
            return createRoute.handler(event, context)
        case 'PUT':
            if (segments.length === 1) {
                const [id] = segments
                event.id = id
                return updateRoute.handler(event, context)
            }
            return {
                statusCode: 500,
                body: `invalid segments in POST request, must be /.netlify/functions/${collection.name}/123456`,
            }

        case 'DELETE':
            if (segments.length === 1) {
                const [id] = segments
                event.id = id
                return deleteRoute.handler(event, context)
            }
            return {
                statusCode: 500,
                body: `invalid segments in DELETE request, must be /.netlify/functions/${collection.name}/123456`,
            }
        default:
            return {
                statusCode: 500,
                body:
                    'unrecognized HTTP Method, must be one of GET/POST/PUT/DELETE',
            }
    }
}