An Example of using FaunaDB with Netlify functions

Netlify + Fauna DB
Using FaunaDB & Netlify functions

https://github.com/netlify/netlify-faunadb-example

https://dashboard.fauna.com/
https://dashboard.fauna.com/db/us/Netlify

https://dashboard.fauna.com/keys/@db/us/Netlify
export FAUNADB_SERVER_SECRET=fnAEcemUZyAAQIFOrKmhE9b52QIGOz0Xqfn50OE1

.nvmrc # v16.13.1

gh repo create fauna-netlify –private
✓ Created repository jasonleonhard/fauna-netlify on GitHub
gh browse

git remote add origin git@github.com:jasonleonhard/fauna-netlify.git
git push -u origin master

ntl init # fauna-netlify
Site Created
Admin URL: https://app.netlify.com/sites/fauna-netlify
URL: https://fauna-netlify.netlify.app
Site ID: add11563-60ec-45c4-90ec-f9a82ea80c68
? Your build command (hugo build/yarn run build/etc): npm run build
? Directory to deploy (blank for current dir): build
? Netlify functions folder: functions
Adding deploy key to repository…

      ›   Error: Failed adding GitHub deploy key with error: Not Found. Does the repository netlify-faunadb-example exist and do netlify has the correct permissions to set up deploy keys?
  Adding deploy key to repository...
  Deploy key added!

  Creating Netlify GitHub Notification Hooks...
  Netlify Notification Hooks configured!

  Success! Netlify CI/CD Configured!

  This site is now configured to automatically deploy from github branches & pull requests

  Next steps:

    git push       Push to your git repository to trigger new site builds
    netlify open   Open the Netlify admin URL of your site

ntl open –admin;
https://fauna-netlify.netlify.app/
ntl open –site
https://app.netlify.com/sites/fauna-netlify

npm run bootstrap

ntl dev
localhost:8888
FaunaDB key is not unauthorized. Make sure you set it in terminal session where you ran npm start. Visit
http://bit.ly/set-fauna-key for more info

https://docs.fauna.com/fauna/current/learn/quick_start/

npm run bootstrap > netlify-fauna@0.1.0 bootstrap > netlify dev:exec node ./scripts/bootstrap-fauna-database.js

◈ No Site ID detected. You probably forgot to run `netlify link` or `netlify init`.
Creating your FaunaDB Database...

Create the fauna database schema!
Fauna Database schema has been created
Claim your fauna database with "netlify addons:auth fauna"

netlify addons:auth fauna # fails

open localhost:3000 && npm start


Netlify + FaunaDB    

Example of using FaunaDB with Netlify functions

Expand Table of Contents

About this application

This application is using React for the frontend, Netlify Functions for API calls, and FaunaDB as the backing database.

Deploy with one click

Click the Deploy to Netlify Button

Setup & Run Locally

  1. Clone down the repository

    git clone https://github.com/netlify/netlify-faunadb-example.git
  2. Enter the repo directory

    cd netlify-faunadb-example
  3. Install the dependencies

    npm install
  4. Sign up for a FaunaDB account

    https://dashboard.fauna.com/accounts/register

  5. Create a database

    In the Fauna Cloud Console:

    • Click “New Database”
    • Enter “Netlify” as the “Database Name”
    • Click “Save”
  6. Create a database access key

    In the Fauna Cloud Console:

    • Click “Security” in the left navigation
    • Click “New Key”
    • Make sure that the “Database” field is set to “Netlify”
    • Make sure that the “Role” field is set to “Admin”
    • Enter “Netlify” as the “Key Name”
    • Click “Save”
  7. Copy the database access key’s secret

    Save the secret somewhere safe; you won’t get a second chance to see it.

  8. Set your database access secret in your terminal environment

    In your terminal, run the following command:

    export FAUNADB_SERVER_SECRET=YourFaunaDBSecretHere

    Replace YourFaunaDBSecretHere with the value of the secret that you copied in the previous step.

  9. Bootstrap your FaunaDB collection and indexes

    npm run bootstrap
  10. Run project locally

    npm start

TLDR; Quick Deploy

  1. Click the Deploy to Netlify button

  1. Click “Connect to GitHub”. Authorize Netlify, when asked.

  2. Paste your FaunaDB database access secret into the “Your FaunaDB Server Secret” field.

  3. Click “Save & Deploy”. Netlify clones your repo, then builds and deploys your app. All done!

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.

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!

  1. Install create react app

    npm install create-react-app -g
  2. Create the react app!

    create-react-app my-app
  3. The react app is now setup!

    # change directories into my-app
    cd my-app

2. Set up FaunaDB

We are using FaunaDB to hold and store all of our todo data.

To setup a FaunaDB account and get the API key we’ll use to scaffold out our todos database, head over to https://dashboard.fauna.com/accounts/register and create a free Fauna Cloud account.

  1. Sign up

  2. Create a key

  3. Name your key and create

  4. Copy this API key for later use, or use the Deploy to Netlify Button and plugin this API key.

  5. Create your FaunaDB database

    Set the FaunaDB API key locally in your terminal

    # on mac
    export FAUNADB_SERVER_SECRET=YourFaunaDBKeyHere
    # on windows
    set FAUNADB_SERVER_SECRET=YourFaunaDBKeyHere

    Replace YourFaunaDBSecretHere with the value of the secret that you copied in the previous step.

    Add the /scripts/bootstrap-fauna-database.js to the root directory of the project. This is an idempotent script that you can run one million times and have the same result (one todos database)

    Next up, add the bootstrap command to npm scripts in your package.json file

    {
      "scripts": {
        "bootstrap": "node ./scripts/bootstrap-fauna-database.js"
      }
    }

    Now we can run the bootstrap command to setup our Fauna database in our FaunaDB account.

    npm run bootstrap

    If you log in to the FaunaDB dashboard you will see your todo database.

3. Create a function

Now, let’s 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 information 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 response 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

Let’s rock and roll.

  1. Create a ./functions directory

    # make functions directory
    #### mdkir functions
    
    mkdir functions # is my guess what is supposed to be correct
  2. 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.

    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.

  3. Add our start & build commands

    Let’s go ahead and add our start & build command to npm scripts in package.json. These will let us run things locally and give a command for Netlify to build our app and functions when we are ready to deploy.

    We are going to be using the npm-run-all npm module to run our frontend and 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 run react-scripts start to run our react app

    The start:server command will run netlify-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 run npm-run-all --parallel start:app start:server to fire them both up at once.

    About npm build

    The build:app command will run react-scripts build to run our React app.

    The build:server command will run netlify-lambda build functions -c ./webpack.config.js to run our function code locally.

    Running npm run build in our terminal will run npm-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": ""
          }
        }
      }
    }
  4. 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 called todos-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_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("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 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.

  1. Read Todos by ID

    Then create a new function file in /functions called todos-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_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),
          });
        });
    };
  2. Read All Todos

    Then create a new function file in /functions called todos-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_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),
          });
        });
    };
  3. Update todo by ID

    Then create a new function file in /functions called todos-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_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),
          });
        });
    };
  4. Delete by ID

    Then create a new function file in /functions called todos-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_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),
          });
        });
    };
  5. Delete batch todos

    Then create a new function file in /functions called todos-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_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

That’s it. You now have your own CRUD API using Netlify 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 JAMstack and we’d love to hear about what you make. Give us a shout about it on Twitter

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 the JSON web token-based Netlify Identity
  • Add in due dates to todos and wire up Functions to notify users via email/SMS
  • File for IPO?

GitHub

View Github