Accessing Your Data With Netlify Functions and React


Static site generators are popular for their speed, security, and user experience. However, sometimes your application needs data that is not available when the site is built. React is a library for building user interfaces that helps you retrieve and store dynamic data in your client application. 

Fauna is a flexible, serverless database delivered as an API that completely eliminates operational overhead such as capacity planning, data replication, and scheduled maintenance. Fauna allows you to model your data as documents, making it a natural fit for web applications written with React. Although you can access Fauna directly via a JavaScript driver, this requires a custom implementation for each client that connects to your database. By placing your Fauna database behind an API, you can enable any authorized client to connect, regardless of the programming language.

Netlify Functions allow you to build scalable, dynamic applications by deploying server-side code that works as API endpoints. In this tutorial, you build a serverless application using React, Netlify Functions, and Fauna. You learn the basics of storing and retrieving your data with Fauna. You create and deploy Netlify Functions to access your data in Fauna securely. Finally, you deploy your React application to Netlify.

Getting started with Fauna

Fauna is a distributed, strongly consistent OLTP NoSQL serverless database that is ACID-compliant and offers a multi-model interface. Fauna also supports document, relational, graph, and temporal data sets from a single query. First, we will start by creating a database in the Fauna console by selecting the Database tab and clicking on the Create Database button.

Next, you will need to create a Collection. For this, you will need to select a database, and under the Collections tab, click on Create Collection.

Fauna uses a particular structure when it comes to persisting data. The design consists of attributes like the example below.

{
  "ref": Ref(Collection("avengers"), "299221087899615749"),
  "ts": 1623215668240000,
  "data": {
    "id": "db7bd11d-29c5-4877-b30d-dfc4dfb2b90e",
    "name": "Captain America",
    "power": "High Strength",
    "description": "Shield"
  }
}

Notice that Fauna keeps a ref column which is a unique identifier used to identify a particular document. The ts attribute is a timestamp to determine the time of creating the record and the data attribute responsible for the data.

Why creating an index is important

Next, let’s create two indexes for our avengers collection. This will be pretty valuable in the latter part of the project. You can create an index from the Index tab or from the Shell tab, which provides a console to execute scripts. Fauna supports two types of querying techniques: FQL (Fauna’s Query language) and GraphQL. FQL operates based on the schema of Fauna, which includes documents, collections, indexes, sets, and databases. 

Let’s create the indexes from the shell.

This command will create an index on the Collection, which will create an index by the id field inside the data object. This index will return a ref of the data object. Next, let’s create another index for the name attribute and name it avenger_by_name.

Creating a server key

To create a server key, we need to navigate the Security tab and click on the New Key button. This section will prompt you to create a key for a selected database and the user’s role.

Getting started with Netlify functions and React

In this section, we’ll see how we create Netlify functions with React. We will be using create-react-app to create the react app.

npx create-react-app avengers-faunadb

After creating the react app, let’s install some dependencies, including Fauna and Netlify dependencies.

yarn add axios bootstrap node-sass uuid faunadb react-netlify-identity react-netlify-identity-widget

Now let’s create our first Netlfiy function. To make the functions, first, we need to install Netlifiy CLI globally.

npm install netlify-cli -g

Now that the CLI is installed, let’s create a .env file on our project root with the following fields.

FAUNADB_SERVER_SECRET= <FaunaDB secret key>
REACT_APP_NETLIFY= <Netlify app url>

Next, Let’s see how we can start with creating netlify functions. For this, we will need to create a directory in our project root called functions and a file called netlify.toml, which will be responsible for maintaining configurations for our Netlify project. This file defines our function’s directory, build directory, and commands to execute.

[build]
command = "npm run build"
functions = "functions/"
publish = "build"

[[redirects]]
  from = "/api/*"
  to = "/.netlify/functions/:splat"
  status = 200
  force = true

We will do some additional configuration for the Netlify configuration file, like in the redirection section in this example. Notice that we are changing the default path of the Netlify function of /.netlify/** to /api/. This configuration is mainly for the improvement of the look and field of the API URL. So to trigger or call our function, we can use the path:

https://domain.com/api/getPokemons

 …instead of:

https://domain.com/.netlify/getPokemons

Next, let’s create our Netlify function in the functions directory. But, first, let’s make a connection file for Fauna called util/connections.js, returning a Fauna connection object.

const faunadb = require('faunadb');
const q = faunadb.query

const clientQuery = new faunadb.Client({
  secret: process.env.FAUNADB_SERVER_SECRET,
});

module.exports = { clientQuery, q };

Next, let’s create a helper function checking for reference and returning since we will need to parse the data on several occasions throughout the application. This file will be util/helper.js.

const responseObj = (statusCode, data) => {
  return {
    statusCode: statusCode,
    headers: {
     /* Required for CORS support to work */
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Headers": "*",
      "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
    },
   body: JSON.stringify(data)
  };
};

const requestObj = (data) => {
  return JSON.parse(data);
}

module.exports = { responseObj: responseObj, requestObj: requestObj }

Notice that the above helper functions handle the CORS issues, stringifying and parsing of JSON data. Let’s create our first function, getAvengers, which will return all the data.

const { responseObj } = require('./util/helper');
const { q, clientQuery } = require('./util/connection');

exports.handler = async (event, context) => {
  try {
   let avengers = await clientQuery.query(
     q.Map(
       q.Paginate(q.Documents(q.Collection('avengers'))),
       q.Lambda(x => q.Get(x))
      )
    )
    return responseObj(200, avengers)
  } catch (error) {
    console.log(error)
    return responseObj(500, error);
  }
};

In the above code example, you can see that we have used several FQL commands like Map, Paginate, Lamda. The Map key is used to iterate through the array, and it takes two arguments: an Array and Lambda. We have passed the Paginate for the first parameter, which will check for reference and return a page of results (an array). Next, we used a Lamda statement, an anonymous function that is quite similar to an anonymous arrow function in ES6.

Next, Let’s create our function AddAvenger responsible for creating/inserting data into the Collection.

const { requestObj, responseObj } = require('./util/helper');
const { q, clientQuery } = require('./util/connection');

exports.handler = async (event, context) => {
  let data = requestObj(event.body);

  try {
    let avenger = await clientQuery.query(
      q.Create(
        q.Collection('avengers'),
        {
          data: {
            id: data.id,
            name: data.name,
            power: data.power,
            description: data.description
          }
        }
      )
    );

    return responseObj(200, avenger)
  } catch (error) {
    console.log(error)
    return responseObj(500, error);
  }
 
};

To save data for a particular collection, we will have to pass, or data to the data:{} object like in the above code example. Then we need to pass it to the Create function and point it to the collection you want and the data. So, let’s run our code and see how it works through the netlify dev command.

Let’s trigger the GetAvengers function through the browser through the URL http://localhost:8888/api/GetAvengers.

The above function will fetch the avenger object by the name property searching from the avenger_by_name index. But, first, let’s invoke the GetAvengerByName function through a Netlify function. For that, let’s create a function called SearchAvenger.

const { responseObj } = require('./util/helper');
const { q, clientQuery } = require('./util/connection');

exports.handler = async (event, context) => {
  const {
    queryStringParameters: { name },
  } = event;

  try {
    let avenger = await clientQuery.query(
      q.Call(q.Function("GetAvengerByName"), [name])
    );
    return responseObj(200, avenger)
  } catch (error) {
    console.log(error)
    return responseObj(500, error);
  }
};

Notice that the Call function takes two arguments where the first parameter will be the reference for the FQL function that we created and the data that we need to pass to the function.

Calling the Netlify function through React

Now that several functions are available let’s consume those functions through React. Since the functions are REST APIs, let’s consume them via Axios, and for state management, let’s use React’s Context API. Let’s start with the Application context called AppContext.js.

import { createContext, useReducer } from "react";
import AppReducer from "./AppReducer"

const initialState = {
    isEditing: false,
    avenger: { name: '', description: '', power: '' },
    avengers: [],
    user: null,
    isLoggedIn: false
};

export const AppContext = createContext(initialState);

export const AppContextProvider = ({ children }) => {
    const [state, dispatch] = useReducer(AppReducer, initialState);

    const login = (data) => { dispatch({ type: 'LOGIN', payload: data }) }
    const logout = (data) => { dispatch({ type: 'LOGOUT', payload: data }) }
    const getAvenger = (data) => { dispatch({ type: 'GET_AVENGER', payload: data }) }
    const updateAvenger = (data) => { dispatch({ type: 'UPDATE_AVENGER', payload: data }) }
    const clearAvenger = (data) => { dispatch({ type: 'CLEAR_AVENGER', payload: data }) }
    const selectAvenger = (data) => { dispatch({ type: 'SELECT_AVENGER', payload: data }) }
    const getAvengers = (data) => { dispatch({ type: 'GET_AVENGERS', payload: data }) }
    const createAvenger = (data) => { dispatch({ type: 'CREATE_AVENGER', payload: data }) }
    const deleteAvengers = (data) => { dispatch({ type: 'DELETE_AVENGER', payload: data }) }

    return <AppContext.Provider value={{
        ...state,
        login,
        logout,
        selectAvenger,
        updateAvenger,
        clearAvenger,
        getAvenger,
        getAvengers,
        createAvenger,
        deleteAvengers
    }}>{children}</AppContext.Provider>
}

export default AppContextProvider;

Let’s create the Reducers for this context in the AppReducer.js file, Which will consist of a reducer function for each operation in the application context.

const updateItem = (avengers, data) => {
    let avenger = avengers.find((avenger) => avenger.id === data.id);
    let updatedAvenger = { ...avenger, ...data };
    let avengerIndex = avengers.findIndex((avenger) => avenger.id === data.id);
    return [
        ...avengers.slice(0, avengerIndex),
        updatedAvenger,
        ...avengers.slice(++avengerIndex),
    ];
}

const deleteItem = (avengers, id) => {
    return avengers.filter((avenger) => avenger.data.id !== id)
}

const AppReducer = (state, action) => {
    switch (action.type) {
        case 'SELECT_AVENGER':
            return {
                ...state,
                isEditing: true,
                avenger: action.payload
            }
        case 'CLEAR_AVENGER':
            return {
                ...state,
                isEditing: false,
                avenger: { name: '', description: '', power: '' }
            }
        case 'UPDATE_AVENGER':
            return {
                ...state,
                isEditing: false,
                avengers: updateItem(state.avengers, action.payload)
            }
        case 'GET_AVENGER':
            return {
                ...state,
                avenger: action.payload.data
            }
        case 'GET_AVENGERS':
            return {
                ...state,
                avengers: Array.isArray(action.payload && action.payload.data) ? action.payload.data : [{ ...action.payload }]
            };
        case 'CREATE_AVENGER':
            return {
                ...state,
                avengers: [{ data: action.payload }, ...state.avengers]
            };
        case 'DELETE_AVENGER':
            return {
                ...state,
                avengers: deleteItem(state.avengers, action.payload)
            };
        case 'LOGIN':
            return {
                ...state,
                user: action.payload,
                isLoggedIn: true
            };
        case 'LOGOUT':
            return {
                ...state,
                user: null,
                isLoggedIn: false
            };
        default:
            return state
    }
}

export default AppReducer;

Since the application context is now available, we can fetch data from the Netlify functions that we have created and persist them in our application context. So let’s see how to call one of these functions.

const { avengers, getAvengers } = useContext(AppContext);

const GetAvengers = async () => {
  let { data } = await axios.get('/api/GetAvengers);
  getAvengers(data)
}

To get the data to the application contexts, let’s import the function getAvengers from our application context and pass the data fetched by the get call. This function will call the reducer function, which will keep the data in the context. To access the context, we can use the attribute called avengers. Next, let’s see how we could save data on the avengers collection.

const { createAvenger } = useContext(AppContext);

const CreateAvenger = async (e) => {
  e.preventDefault();
  let new_avenger = { id: uuid(), ...newAvenger }
  await axios.post('/api/AddAvenger', new_avenger);
  clear();
  createAvenger(new_avenger)
}

The above newAvenger object is the state object which will keep the form data. Notice that we pass a new id of type uuid to each of our documents. Thus, when the data is saved in Fauna, We will be using the createAvenger function in the application context to save the data in our context. Similarly, we can invoke all the netlify functions with CRUD operations like this via Axios.

How to deploy the application to Netlify

Now that we have a working application, we can deploy this app to Netlify. There are several ways that we can deploy this application:

  1. Connecting and deploying the application through GitHub
  2. Deploying the application through the Netlify CLI

Using the CLI will prompt you to enter specific details and selections, and the CLI will handle the rest. But in this example, we will be deploying the application through Github. So first, let’s log in to the Netlify dashboard and click on New Site from Git button. Next, It will prompt you to select the Repo you need to deploy and the configurations for your site like build command, build folder, etc.

How to authenticate and authorize functions by Netlify Identity

Netlify Identity provides a full suite of authentication functionality to your application which will help us to manage authenticated users throughout the application. Netlify Identity can be integrated easily into the application without using any other 3rd party service and libraries. To enable Netlify Identity, we need to login into our Neltify dashboard, and under our deployed site, we need to move to the Identity tab and allow the identity feature.

Enabling Identity will provide a link to your netlify identity. You will have to copy that URL and add it to the .env file of your application for REACT_APP_NETLIFY. Next, We need to add the Netlify Identity to our React application through the netlify-identity-widget and the Netlify functions. But, first, let’s add the REACT_APP_NETLIFY property for the Identity Context Provider component in the index.js file.

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import "react-netlify-identity-widget/styles.css"
import 'bootstrap/dist/css/bootstrap.css';
import App from './App';
import { IdentityContextProvider } from "react-netlify-identity-widget"
const url = process.env.REACT_APP_NETLIFY;

ReactDOM.render(
  <IdentityContextProvider url={url}>
    <App />
  </IdentityContextProvider>,
  document.getElementById('root')
);

This component is the Navigation bar that we use in this application. This component will be on top of all the other components to be the ideal place to handle the authentication. This react-netlify-identity-widget will add another component that will handle the user signI= in and sign up.

Next, let’s use the Identity in our Netlify functions. Identity will introduce some minor modifications to our functions, like the below function GetAvenger.

const { responseObj } = require('./util/helper');
const { q, clientQuery } = require('./util/connection');

exports.handler = async (event, context) => {
    if (context.clientContext.user) {
        const {
            queryStringParameters: { id },
        } = event;
        try {
            const avenger = await clientQuery.query(
                q.Get(
                    q.Match(q.Index('avenger_by_id'), id)
                )
            );
            return responseObj(200, avenger)
        } catch (error) {
            console.log(error)
            return responseObj(500, error);
        }
    } else {
        return responseObj(401, 'Unauthorized');
    }
};

The context of each request will consist of a property called clientContext, which will consist of authenticated user details. In the above example, we use a simple if condition to check the user context. 

To get the clientContext in each of our requests, we need to pass the user token through the Authorization Headers. 

const { user } = useIdentityContext();

const GetAvenger = async (id) => {
  let { data } = await axios.get('/api/GetAvenger/?id=' + id, user && {
    headers: {
      Authorization: `Bearer ${user.token.access_token}`
    }
  });
  getAvenger(data)
}

This user token will be available in the user context once logged in to the application through the netlify identity widget.

As you can see, Netlify functions and Fauna look to be a promising duo for building serverless applications. You can follow this GitHub repo for the complete code and refer to this URL for the working demo.

Conclusion

In conclusion, Fauna and Netlify look to be a promising duo for building serverless applications. Netlify also provides the flexibility to extend its functionality through the plugins to enhance the experience. The pricing plan with pay as you go is ideal for developers to get started with fauna. Fauna is extremely fast, and it auto-scales so that developers will have the time to focus on their development more than ever. Fauna can handle complex database operations where you would find in Relational, Document, Graph, Temporal databases. Fauna Driver support all the major languages such as Android, C#, Go, Java, JavaScript, Python, Ruby, Scala, and Swift. With all these excellent features, Fauna looks to be one of the best Serverless databases. For more information, go through Fauna documentation.



Source link