Netlify + FaunaDB
Example of using FaunaDB with Netlify functions
About this application
This application is using React for the frontend, Netlify Functions for API calls, and FaunaDB as the backing database.
Setup & Run Locally
-
Clone down the repository
git clone git@github.com:netlify/netlify-faunadb-example.git
-
Install the dependencies
npm install
-
Run project locally
npm start
TLDR; Quick Deploy
- Click the Deploy to Netlify Button
Tutorial
Background
This application is using React for the frontend, Netlify Functions for API calls, and FaunaDB as the backing database.
We are going to explore how to get up and running with netlify functions and how to deploy your own serverless backend.
So, lets dive right in!
1. Create React app
We are using React for this demo app, but you can use whatever you want to manage the frontend.
Into VueJS? Awesome use that.
Miss the days of jQuery? Righto jQuery away!
Fan of vanillaJS? By all means, have at it!
-
Install create react app
npm install create-react-app -g
-
Create the react app!
create-react-app my-app
-
The react app is now setup!
# change directories into my-app cd my-app # start the app npm start
2. Create a function
Now, lets create a function for our app and wire that up to run locally.
The functions in our project are going to live in a /functions
folder. You can set this to whatever you'd like but we like the /functions
convention.
Anatomy of a Lambda function
All AWS Lambda functions have the following signature:
exports.handler = (event, context, callback) => {
// "event" has informatiom about the path, body, headers etc of the request
console.log('event', event)
// "context" has information about the lambda environment and user details
console.log('context', context)
// The "callback" ends the execution of the function and returns a reponse back to the caller
return callback(null, {
statusCode: 200,
body: JSON.stringify({
data: '⊂◉‿◉つ'
})
})
}
We are going to use the faunadb
npm package to connect to our Fauna Database and create an item
Setting up functions for local development
Lets rock and roll.
-
Create a
./functions
directory# make functions directory mdkir functions
-
Install
netlify-lambda
Netlify lambda is a tool for locally emulating the serverless function for development and for bundling our serverless function with third party npm modules (if we are using those)
npm i netlify-lambda --save-dev
To simulate our function endpoints locally, we need to setup a proxy for webpack to use.
Note: If you are using CreateReactApp v2 and above, follow the following CRA v2 and above steps to add proxy settings, else continue to CRA below v2
CRA v2 and above
- Install http-proxy-middleware
npm i --save http-proxy-middleware
- Create a setupProxy.js file inside your src folder
- Put the following code inside
setupProxy.js
const proxy = require('http-proxy-middleware'); module.exports = function(app) { app.use(proxy('/.netlify/functions/', { target: 'http://localhost:9000/', "pathRewrite": { "^/\\.netlify/functions": "" } })); };
CRA below v2
In
package.json
add:{ "name": "react-lambda", ... "proxy": { "/.netlify/functions": { "target": "http://localhost:9000", "pathRewrite": { "^/\\.netlify/functions": "" } } } }
This will proxy requests we make to
/.netlify/functions
to our locally running function server at port 9000. - Install http-proxy-middleware
-
Add our
start
&build
commandsLets go ahead and add our
start
&build
command to npm scripts inpackage.json
. These will let us running things locally and give a command for netlify to run to build our app & functions when we are ready to deploy.We are going to be using the
npm-run-all
npm module to run our frontend & backend in parallel in the same terminal window.So install it!
npm install npm-run-all --save-dev
About
npm start
The
start:app
command will runreact-scripts start
to run our react appThe
start:server
command will runnetlify-lambda serve functions -c ./webpack.config.js
to run our function code locally. The-c webpack-config
flag lets us set a custom webpack config to fix a module issue with faunaDB module.Running
npm start
in our terminal will runnpm-run-all --parallel start:app start:server
to fire them both up at once.About
npm build
The
build:app
command will runreact-scripts build
to run our react appThe
build:server
command will runnetlify-lambda build functions -c ./webpack.config.js
to run our function code locally.Running
npm run build
in our terminal will runnpm-run-all --parallel build:**
to fire them both up at once.Your
package.json
should look like{ "name": "netlify-fauna", "scripts": { "👇 ABOUT-bootstrap-command": "💡 scaffold and setup FaunaDB #", "bootstrap": "node ./scripts/bootstrap-fauna-database.js", "👇 ABOUT-start-command": "💡 start the app and server #", "start": "npm-run-all --parallel start:app start:server", "start:app": "react-scripts start", "start:server": "netlify-lambda serve functions -c ./webpack.config.js", "👇 ABOUT-prebuild-command": "💡 before 'build' runs, run the 'bootstrap' command #", "prebuild": "echo 'setup faunaDB' && npm run bootstrap", "👇 ABOUT-build-command": "💡 build the react app and the serverless functions #", "build": "npm-run-all --parallel build:**", "build:app": "react-scripts build", "build:functions": "netlify-lambda build functions -c ./webpack.config.js", }, "dependencies": { "faunadb": "^0.2.2", "react": "^16.4.0", "react-dom": "^16.4.0", "react-scripts": "1.1.4" }, "devDependencies": { "netlify-lambda": "^0.4.0", "npm-run-all": "^4.1.3" }, "proxy": { "/.netlify/functions": { "target": "http://localhost:9000", "pathRewrite": { "^/\\.netlify/functions": "" } } } }
-
Install FaunaDB and write the create function
We are going to be using the
faunadb
npm module to call into our todos index in FaunaDB.So install it in the project
npm i faunadb --save
Then create a new function file in
/functions
calledtodos-create.js
/* code from functions/todos-create.js */ import faunadb from 'faunadb' /* Import faunaDB sdk */ /* configure faunaDB Client with our secret */ const q = faunadb.query const client = new faunadb.Client({ secret: process.env.FAUNADB_SERVER_SECRET }) /* export our lambda function as named "handler" export */ exports.handler = (event, context, callback) => { /* parse the string body into a useable JS object */ const data = JSON.parse(event.body) console.log('Hello webinar. Function `todo-create` invoked', data) const todoItem = { data: data } /* construct the fauna query */ return client.query(q.Create(q.Ref('classes/todos'), todoItem)) .then((response) => { console.log('success', response) /* Success! return the response with statusCode 200 */ return callback(null, { statusCode: 200, body: JSON.stringify(response) }) }).catch((error) => { console.log('error', error) /* Error! return the error with statusCode 400 */ return callback(null, { statusCode: 400, body: JSON.stringify(error) }) }) }
4. Connect the function to the frontend app
Inside of the react app, we can now wire up the /.netlify/functions/todos-create
endpoint to an AJAX request.
// Function using fetch to POST to our API endpoint
function createTodo(data) {
return fetch('/.netlify/functions/todos-create', {
body: JSON.stringify(data),
method: 'POST'
}).then(response => {
return response.json()
})
}
// Todo data
const myTodo = {
title: 'My todo title',
completed: false,
}
// create it!
createTodo(myTodo).then((response) => {
console.log('API response', response)
// set app state
}).catch((error) => {
console.log('API error', error)
})
Requests to /.netlify/function/[Function-File-Name]
will work seamlessly on localhost and on the live site because we are using the local proxy with webpack.
We will be skipping over the rest of the frontend parts of the app because you can use whatever framework you'd like to build your application.
All the demo React frontend code is available here
5. Finishing the Backend Functions
So far we have created our todo-create
function done and we've seen how we make requests to our live function endpoints. It's now time to add the rest of our CRUD functions to manage our todos.
-
Read Todos by ID
Then create a new function file in
/functions
calledtodos-read.js
/* code from functions/todos-read.js */ import faunadb from 'faunadb' import getId from './utils/getId' const q = faunadb.query const client = new faunadb.Client({ secret: process.env.FAUNADB_SERVER_SECRET }) exports.handler = (event, context, callback) => { const id = getId(event.path) console.log(`Function 'todo-read' invoked. Read id: ${id}`) return client.query(q.Get(q.Ref(`classes/todos/${id}`))) .then((response) => { console.log('success', response) return callback(null, { statusCode: 200, body: JSON.stringify(response) }) }).catch((error) => { console.log('error', error) return callback(null, { statusCode: 400, body: JSON.stringify(error) }) }) }
-
Read All Todos
Then create a new function file in
/functions
calledtodos-read-all.js
/* code from functions/todos-read-all.js */ import faunadb from 'faunadb' const q = faunadb.query const client = new faunadb.Client({ secret: process.env.FAUNADB_SERVER_SECRET }) exports.handler = (event, context, callback) => { console.log('Function `todo-read-all` invoked') return client.query(q.Paginate(q.Match(q.Ref('indexes/all_todos')))) .then((response) => { const todoRefs = response.data console.log('Todo refs', todoRefs) console.log(`${todoRefs.length} todos found`) // create new query out of todo refs. http://bit.ly/2LG3MLg const getAllTodoDataQuery = todoRefs.map((ref) => { return q.Get(ref) }) // then query the refs return client.query(getAllTodoDataQuery).then((ret) => { return callback(null, { statusCode: 200, body: JSON.stringify(ret) }) }) }).catch((error) => { console.log('error', error) return callback(null, { statusCode: 400, body: JSON.stringify(error) }) }) }
-
Update todo by ID
Then create a new function file in
/functions
calledtodos-update.js
/* code from functions/todos-update.js */ import faunadb from 'faunadb' import getId from './utils/getId' const q = faunadb.query const client = new faunadb.Client({ secret: process.env.FAUNADB_SERVER_SECRET }) exports.handler = (event, context, callback) => { const data = JSON.parse(event.body) const id = getId(event.path) console.log(`Function 'todo-update' invoked. update id: ${id}`) return client.query(q.Update(q.Ref(`classes/todos/${id}`), {data})) .then((response) => { console.log('success', response) return callback(null, { statusCode: 200, body: JSON.stringify(response) }) }).catch((error) => { console.log('error', error) return callback(null, { statusCode: 400, body: JSON.stringify(error) }) }) }
-
Delete by ID
Then create a new function file in
/functions
calledtodos-delete.js
/* code from functions/todos-delete.js */ import faunadb from 'faunadb' import getId from './utils/getId' const q = faunadb.query const client = new faunadb.Client({ secret: process.env.FAUNADB_SERVER_SECRET }) exports.handler = (event, context, callback) => { const id = getId(event.path) console.log(`Function 'todo-delete' invoked. delete id: ${id}`) return client.query(q.Delete(q.Ref(`classes/todos/${id}`))) .then((response) => { console.log('success', response) return callback(null, { statusCode: 200, body: JSON.stringify(response) }) }).catch((error) => { console.log('error', error) return callback(null, { statusCode: 400, body: JSON.stringify(error) }) }) }
-
Delete batch todos
Then create a new function file in
/functions
calledtodos-delete-batch.js
/* code from functions/todos-delete-batch.js */ import faunadb from 'faunadb' import getId from './utils/getId' const q = faunadb.query const client = new faunadb.Client({ secret: process.env.FAUNADB_SERVER_SECRET }) exports.handler = (event, context, callback) => { const data = JSON.parse(event.body) console.log('data', data) console.log('Function `todo-delete-batch` invoked', data.ids) // construct batch query from IDs const deleteAllCompletedTodoQuery = data.ids.map((id) => { return q.Delete(q.Ref(`classes/todos/${id}`)) }) // Hit fauna with the query to delete the completed items return client.query(deleteAllCompletedTodoQuery) .then((response) => { console.log('success', response) return callback(null, { statusCode: 200, body: JSON.stringify(response) }) }).catch((error) => { console.log('error', error) return callback(null, { statusCode: 400, body: JSON.stringify(error) }) }) }
After we deploy all these functions, we will be able to call them from our frontend code with these fetch calls:
/* Frontend code from src/utils/api.js */
/* Api methods to call /functions */
const create = (data) => {
return fetch('/.netlify/functions/todos-create', {
body: JSON.stringify(data),
method: 'POST'
}).then(response => {
return response.json()
})
}
const readAll = () => {
return fetch('/.netlify/functions/todos-read-all').then((response) => {
return response.json()
})
}
const update = (todoId, data) => {
return fetch(`/.netlify/functions/todos-update/${todoId}`, {
body: JSON.stringify(data),
method: 'POST'
}).then(response => {
return response.json()
})
}
const deleteTodo = (todoId) => {
return fetch(`/.netlify/functions/todos-delete/${todoId}`, {
method: 'POST',
}).then(response => {
return response.json()
})
}
const batchDeleteTodo = (todoIds) => {
return fetch(`/.netlify/functions/todos-delete-batch`, {
body: JSON.stringify({
ids: todoIds
}),
method: 'POST'
}).then(response => {
return response.json()
})
}
export default {
create: create,
readAll: readAll,
update: update,
delete: deleteTodo,
batchDelete: batchDeleteTodo
}
Wrapping Up
I hope you have enjoyed this tutorial on building your own CRUD API using Netlify serverless functions and FaunaDB.
As you can see, functions can be extremely powerful when combined with a cloud database!
The sky is the limit on what you can build with the JAM stack and we'd love to hear about what you make.
Next Steps
This example can be improved with users/authentication. Next steps to build out the app would be:
- Add in the concept of users for everyone to have their own todo list
- Wire up authentication using Netlify Identity JWTs
- Add in due dates to todos and wire up Functions to notify users via email/SMS
- File for IPO?