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:
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.
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.
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
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
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.
The Netlify CLI has templates available that can simply these steps. It's well taking the time to examine the templates offered.
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"
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:
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
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.
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),
}
})
}
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),
}
})
}
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',
}
}
}