Vivek Sonar's Blog

Vivek Sonar's Blog

Building Fullstack Serverless Applications using Fauna

Building Fullstack Serverless Applications using Fauna

Introduction

Today we will build a Full Stack Serverless Application with Netlify Functions, GraphQL, Fauna, and ReactJS. This will be a hands-on and easy to follow tutorial. So I encourage you to follow along and get your hands dirty with serverless.

Prerequisites

  • No Previous experience with Fauna is mandatory.
  • NodeJS v12.0 or higher installed on the local machine.
  • Having a good internet connection will be helpful following along with this tutorial.
  • A code editor of your choice.

And that's it. Let's start by building our application.

What are we building?

We will build a Product Review application for Fauna, where you can leave a review about Fauna.

  • We will store all the reviews in the database.
  • Users need to authenticate before posting any review.
  • We will create CRUD APIs with GraphQL to interact with FaunaDB.
  • No need to spin any servers (Virtual Machines) to host the APIs, as the application will be completely serverless.

We will develop and deploy an entire application from scratch. Having a little experience with Fauna might be helpful but not mandatory.

Fauna

Fauna is a serverless global database designed for low latency and developer productivity. It has proven to be appealing to Jamstack developers for its global scalability, native GraphQL API, and FQL query language. It is a distributed, strongly consistent OLTP NoSQL database that is ACID-compliant and offers a multi-model interface.

It has an active-active architecture and can span clouds and continents. FaunaDB supports document, relational, graph, and temporal data sets from a single query. Besides its FQL query language, the product supports GraphQL, with SQL planned for the future. Basically, you don't have to manage the infrastructure behind the Fauna. Just spin it up and start using it in seconds.

Going further in this article, you'll get to see how easy it is to provision a Fauna cluster and use it.

Setting up Fauna(Database)

  • Go ahead and register/sign in with an Email or GitHub, or Netlify.
  • Click on create database and give it any preferred name.

Screenshot (227)

create-database

Now that we have our database created, we can go ahead and start building our backend. We will start by defining our GraphQL schema. We will use Fauna’s default GraphQL engine to import the schema into our database.

  • Create a faunareview.gql file. In this file, we will define our schema.
  • Add this snippet in the file.
type Review {
    name: String!
    text: String!
    rating: Int!
 }

 type Query {
    allReviews: [Review!]!
 }
  • Copy this simple schema and save the file.

Go ahead to the GraphQL playground section of your database and import the schema.

graphql-schema-min

  • That's it; our database has been successfully created and is ready to use.
  • Let's try out some queries against our database using GraphQL and see whether it is working or not.

queries_graphql

  • The mutation command puts the data passed in the query to the database.
    mutation {
      createReview(data: {name:"Jhon" text: "FaunaDB is so cool", rating: 5 }) {
         _id
         name
          text
          rating
       }
    }
    
  • Query reads the data from the database.
    query {
      allReviews {
        data {
           _id
           name
           text
           rating
        }
      }
    }
    

Excellent, now that our database is fully operational, we can use it to create APIs to interact with the database. But before that, to connect to our database, we need a secret.

  • Go to security and create a key which we will use to connect and interact with our database.
  • Make sure you store the key somewhere safe and don't share it with anyone.
  • Make a .env file and store your secret key there. FAUNA_ADMIN_SECRET=YOUR_SECRETE_KEY

Screenshot (228)

  • Also create a gitignore and include .env and node_modules in it.

Creating Backend

Now that we are done with setting up the database and its secrets in our working directory, we can go ahead and start creating serverless functions. And for hosting this function, we are going to use Netlify Function. Netlify Function: Setting up AWS Lambda functions on your own can be somewhat complicated. Tutorials average 20 steps! With Netlify, write your Lambda functions with JavaScript or GO and drop them into your functions folder. Everything else is automatic. Before creating and using the Netlify Functions, we need to install some dependencies. So go ahead and create a Netlify account. After making an account go ahead and open the command prompt/terminal in your root directory.

npm init

To work with the netlify functions, we need to install netlify-cli so add it is using:

npm install -g netlify-cli

We will also need axios for making HTTP requests and dotenv to use the env variables.

npm install axios dotenv

That's it we can start creating functions now.

Creating Functions

For every function that we want to run on netlify, we need to make it under the functions directory.

mkdir functions

Go ahead and create one more folder named utils under the functions directory. In the utils directory, create a query.js file where we will make a call to our graphql schema.

const axios = require("axios");
 require("dotenv").config();
 module.exports = async (query, variables) => {
   const result = await axios({
       url: "https://graphql.fauna.com/graphql",
       method: "POST",
       headers: {
           Authorization: `Bearer ${process.env.FAUNA_ADMIN_SECRET}`
       },
       data: {
         query,
         variables
       }
  });
  return result.data;
 };

Under the functions directory, we will create get-reviews.js function. This function will make a GET request and will return all the reviews in JSON format.

// get-reviews.js

const query = require("./utils/query");

const GET_REVIEWS = `
    query {
        allReviews {
          data {
             _id
             name
             text
             rating
          }
        }
     }
`;

 exports.handler = async () => {
    const { data, errors } = await query(GET_REVIEWS);

    if (errors) {
       return {
         statusCode: 500,
         body: JSON.stringify(errors)
       };
    }

    return {
      statusCode: 200,
      body: JSON.stringify({ messages: data.allReviews.data })
    };
  };

Let's test this function. You need to login to netlify before you can test these functions on your local machine. You can do this by.

netlify login

Make sure you click authorize when you get redirected to netlify website.

After that, create a netlify.toml file in the root directory (outside functions directory). This is the code for netlify.toml file

 [build]
    functions = "functions"

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

netlify.toml file is used to tell netlify about the location of the functions we have written so that it is known at the build time. Netlify automatically provides the APIs for the functions. The URL to access the API is in this form, /.netlify/functions/get-reviews which may not be very user-friendly. We have written a redirect to make it like, /api/get-reviews.

Let's run/test the functions locally. To run/test the functions locally, do.

netlify dev

You can view the response on port 8888.

http://localhost:8888/api/get-reviews

Screenshot (233)

Now that it’s working,let’s create a function to create a review now. Again in the functions directory, create a new file named create-reviews.js.

// create-review.js

const query = require("./utils/query");

const CREATE_REVIEW = `
  mutation($name: String!, $text: String!, $rating: Int!){
    createReview(data: {name: $name, text: $text, rating: $rating}){
      _id
      name
      text
      rating
    }
  }
`;

exports.handler = async event => {
  const { name, text, rating } = JSON.parse(event.body);
  const { data, errors } = await query(
          CREATE_REVIEW, { 
name, text, rating });

  if (errors) {
    return {
      statusCode: 500,
      body: JSON.stringify(errors)
    };
  }

  return {
    statusCode: 200,
    body: JSON.stringify({ review: data.createReview })
  };
};

To test it we can use tools like Postman. Pass name text you want to insert and rating in JSON format and test the API. It will add the record to our database. And the results will look something like this.

Screenshot (234)

Now that we have the create-review function let's create an update-review function. In the update review function we will take the id of the component and updated text and store it in the database.

// update-review.js

const query = require("./utils/query");

const UPDATE_REVIEW = `
    mutation($id: ID!,$name: String!, $text: String!, $rating: Int!){
        updateReview(id: $id, data: {name: $name, text: $text, rating: $rating}){
            _id
            name
            text
            rating
        }
    }
`;

exports.handler = async event => {
  const { id,name, text, rating } = JSON.parse(event.body);
  const { data, errors } = await query(
       UPDATE_REVIEW, { id,name, text, rating });

  if (errors) {
    return {
      statusCode: 500,
      body: JSON.stringify(errors)
    };
  }

  return {
    statusCode: 200,
    body: JSON.stringify({ updatedReview: 
data.updateReview })
  };
};

update-function

Now let's quickly create a delete-review function to delete the unwanted reviews.

// delete-review.js
const query = require("./utils/query");

const DELETE_REVIEW = `
  mutation($id: ID!) {
    deleteReview(id: $id){
      _id
    }
  }
`;

exports.handler = async event => {
  const { id } = JSON.parse(event.body);
  const { data, errors } = await query(
    DELETE_REVIEW, { id });

  if (errors) {
    return {
      statusCode: 500,
      body: JSON.stringify(errors)
    };
  }

  return {
    statusCode: 200,
    body: JSON.stringify({ deletedReview: data.deleteReview
   })
  };
};

Add Delete GIF

After everything we have done, the directory structure should look something like this.

And we are done with our backend. I hope that after following till here, you have got an idea on how to setup the Fauna database, working with serverless functions and GraphQL functions. You can also host the functions on AWS lambda, but netlify functions make our life easier. Screenshot (239)

Building the Frontend:

For building out frontend, we will be using Gatsby. Gatsby is an easy way to bootstrap a React application that outputs a static website (simple HTML file) with your application pre-rendered for optimal delivery.

  • So make sure you install gatsby-cli globally.
    npm install -g gatsby-cli
    
    After the gatsby-cli is installed, we should add react, react-dom, and gatsby to start building the application's frontend.
    npm install react react-dom gatsby --save
    

Now we need to add a script in package.json so every time we run develop, it calls gatsby develop. Add the develop script in package.json.

 "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "develop": "gatsby develop"
  }

Gatsby needs its own configuration file called gatsby-config.js. So go ahead and create a file. And for now, we will not have anything to add to this file. We will later modify this file.

 module.exports = {
  // Let it be empty for now
 }

Start the development server first using.

netlify dev

And you can view the results on http://localhost:8888.

Now let's create our frontend. So, to begin with, let's understand the directory structure we will work with. We will create an src directory in the root folder, and there will be a pages folder in the src directory, which will contain all our frontend code. So create an src folder in the root folder and pages folder inside the src folder.

Now that we have created the proper folder structure to test if it's working or not let's create a simple hello world page in index.js.

import React, { useEffect, useState } from 'react';    

export default () => {    
    const [status, setStatus ] = useState('loading...');  
    const [reviews, setReviews] = useState(null);

  return (
    <>    
      <h1>Hello World...</h1>
    </>        
  )    
}

The output should look something like this.

Screenshot (254)

This is a simple React component that has two inbuilt react hooks called useState and useEffect. We will require this to keep track of our review array in our application. Also, make sure to add cache and public folders to .gitignore. These are produced by Gatsby and are not required.

To do this add the following lines to .gitignore.

.cache
public
node_modules
*.env

Loading the data from Fauna to React App.

We need to load all the data we get from /api/get-reviews and load it in the application to display each one in the form of a card. For fetching the data from the API, we will use axios. No need to install it because we did it already in the start while building the backend. But import it in index.js

import axios from "axios";

Now to make a call to the serverless function using the URL /api/get-reviews, we will use the useEffect hook.

export default () => {
  const [status, setStatus] = useState("loading...");
  const [reviews, setReviews] = useState(null);
  useEffect(() => {
    if (status !== "loading...") return;
    axios("/api/get-reviews").then((result) => {
      if (result.status !== 200) {
        console.error("Error loading Reviews");
        console.error(result);
        return;
      }
      setReviews(result.data.reviews);
      setStatus("loaded");
    });
  }, [status]);
  • On a successful response, the reviews are stored in the reviews state variable.
  • We will use a 3rd party API to get random cool looking avatars as a profile picture for every review.
  • We can add this code snippet to fetch the image, which calls the 3rd party API and selects a random avatar.
const getAvatar = () => {
  const random = Math.floor(Math.random() * (testimonials.length - 0 + 1) + 0);
  const imgUrl = `https://avatars.dicebear.com/api/human/${random}.svg?mood[]=happy`;
  return imgUrl;
}

Let render everything and check if everything is working as it should. No need to add styles. We will render the values and check if the data is displayed properly or not.

return (
    <>
    {reviews && reviews.map((review, index) => (
      <div key={ index }>
        <img 
          src={ getAvatar() } 
          height="50px"
          width="50px"
          alt="avatar" />
        <div className="testimonial">
        <p className="text">
            { review.name }
          </p>
          <span>{ review.rating }</span>
          <p className="text">
            { review.text }
          </p>
        </div>
      </div>
    ))}
    </>
  );

The full index.js at this point will look something like this.

// Full index.js
import React, { useEffect, useState } from "react";
import axios from "axios";

export default () => {
  const [status, setStatus] = useState("loading...");
  const [reviews, setReviews] = useState(null);
  useEffect(() => {
    if (status !== "loading...") return;
    axios("/api/get-reviews").then((result) => {
      if (result.status !== 200) {
        console.error("Error loading Reviews");
        console.error(result);
        return;
      }
      setReviews(result.data.reviews);
      setStatus("loaded");
    });
  }, [status]);
  const getAvatar = () => {
    const random = Math.floor(Math.random() * (reviews.length - 0 + 1) + 0);
    const imgUrl = `https://avatars.dicebear.com/api/human/${random}.svg?mood[]=happy`;
    return imgUrl;
  };

  return (
    <>
    {reviews && reviews.map((review, index) => (
      <div key={ index }>
        <img 
          src={ getAvatar() } 
          height="50px"
          width="50px"
          alt="avatar" />
        <div className="testimonial">
        <p className="text">
            { review.name }
          </p>
          <span>{ review.rating }</span>
          <p className="text">
            { review.text }
          </p>
        </div>
      </div>
    ))}
    </>
  );
};

And the app will look like this. Screenshot (256)

Now that we know the data is being fetched and appropriately displayed let's make it look better. Now that we know the data is being fetched and appropriately displayed let's make it look better.

npm install react-stars material-ui react-bootstrap --save

We will use react-start to style our rating component. We will also use cards from material-ui to give the reviews a better appearance. And we will use the react-bootstrap to create a modal to add new reviews after the user is authenticated. After we install the libraries, let's import them to index.js

import ReactStars from "react-stars";
import {
  Grid,
  makeStyles,
  CardContent,
  CardMedia,
  Button,
  CardActionArea,
  Card,
  Typography,
  CardActions,
} from "@material-ui/core";
import Modal from "react-bootstrap/Modal";

Now, let's add the card component that we need to display our review data. We will simply wrap the JSX in the card component and use the react starts to display our ratings.

return (
    <>
      <div className="container1">
        <div>
          <Grid
            container
            spacing={4}
            className={classes.gridContainer}
            justify="center"
          >
            {reviews &&
              reviews.map((review, index) => (
                <Grid item xs={12} sm={6} md={4}>
                  <Card
                    className={classesCards.root}
                    className="card"
                    variant="outlined"
                  >
                    <CardActionArea>
                      <CardMedia
                        className={classesCards.media}
                        height="10"
                        image={getAvatar()}
                        title="FaunaDB Users"
                      />
                      <CardContent>
                        <Typography
                          className={classesCards.content}
                          gutterBottom
                          variant="h4"
                          component="h2"
                        >
                          {review.name}
                        </Typography>
                        <Typography
                          variant="h4"
                          color="textSecondary"
                          component="h2"
                        >
                          {review.text}
                        </Typography>
                        <Typography variant="h5" component="h2">
                          <ReactStars
                            className="rating"
                            count={review.rating}
                            size={24}
                            color1={"#ffd700"}
                            edit={false}
                            half={false}
                          />
                          <br />
                        </Typography>
                      </CardContent>
                    </CardActionArea>
                  </Card>
                </Grid>
              ))}
          </Grid>
        </div>
      </div>

    </>
  );

We are also using makeStyle from material-ui/core here, which we need to define. This will have the styling we need for our card component. Add this snippet to index.js.

const useStyles = makeStyles({
    gridContainer: {
      paddingLeft: "40px",
      paddingRight: "40px"
    }
  });
  const useStylesCards = makeStyles({
    root: {
      minWidth: 200
    },
    bullet: {
      display: "inline-block",
      margin: "0 2px",
      transform: "scale(0.8)"
    },
    title: {
      fontSize: 14
    },
    pos: {
      marginBottom: 12
    },
    content:{
      flexGrow: 1,
      align: "center"
    },
    media: {
      height: 70,
      paddingTop: '56.25%', // 16:9
    }
  });

  // For using css values in card components
  const classes = useStyles();
  const classesCards = useStylesCards();

We need some styling to make the appearance look better. So add index.css file in src/pages directory and add these lines of code.

body {
    background-color: rgba(0, 183, 255, 1);
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='540' height='450' viewBox='0 0 1080 900'%3E%3Cg fill-opacity='.1'%3E%3Cpolygon fill='%23444' points='90 150 0 300 180 300'/%3E%3Cpolygon points='90 150 180 0 0 0'/%3E%3Cpolygon fill='%23AAA' points='270 150 360 0 180 0'/%3E%3Cpolygon fill='%23DDD' points='450 150 360 300 540 300'/%3E%3Cpolygon fill='%23999' points='450 150 540 0 360 0'/%3E%3Cpolygon points='630 150 540 300 720 300'/%3E%3Cpolygon fill='%23DDD' points='630 150 720 0 540 0'/%3E%3Cpolygon fill='%23444' points='810 150 720 300 900 300'/%3E%3Cpolygon fill='%23FFF' points='810 150 900 0 720 0'/%3E%3Cpolygon fill='%23DDD' points='990 150 900 300 1080 300'/%3E%3Cpolygon fill='%23444' points='990 150 1080 0 900 0'/%3E%3Cpolygon fill='%23DDD' points='90 450 0 600 180 600'/%3E%3Cpolygon points='90 450 180 300 0 300'/%3E%3Cpolygon fill='%23666' points='270 450 180 600 360 600'/%3E%3Cpolygon fill='%23AAA' points='270 450 360 300 180 300'/%3E%3Cpolygon fill='%23DDD' points='450 450 360 600 540 600'/%3E%3Cpolygon fill='%23999' points='450 450 540 300 360 300'/%3E%3Cpolygon fill='%23999' points='630 450 540 600 720 600'/%3E%3Cpolygon fill='%23FFF' points='630 450 720 300 540 300'/%3E%3Cpolygon points='810 450 720 600 900 600'/%3E%3Cpolygon fill='%23DDD' points='810 450 900 300 720 300'/%3E%3Cpolygon fill='%23AAA' points='990 450 900 600 1080 600'/%3E%3Cpolygon fill='%23444' points='990 450 1080 300 900 300'/%3E%3Cpolygon fill='%23222' points='90 750 0 900 180 900'/%3E%3Cpolygon points='270 750 180 900 360 900'/%3E%3Cpolygon fill='%23DDD' points='270 750 360 600 180 600'/%3E%3Cpolygon points='450 750 540 600 360 600'/%3E%3Cpolygon points='630 750 540 900 720 900'/%3E%3Cpolygon fill='%23444' points='630 750 720 600 540 600'/%3E%3Cpolygon fill='%23AAA' points='810 750 720 900 900 900'/%3E%3Cpolygon fill='%23666' points='810 750 900 600 720 600'/%3E%3Cpolygon fill='%23999' points='990 750 900 900 1080 900'/%3E%3Cpolygon fill='%23999' points='180 0 90 150 270 150'/%3E%3Cpolygon fill='%23444' points='360 0 270 150 450 150'/%3E%3Cpolygon fill='%23FFF' points='540 0 450 150 630 150'/%3E%3Cpolygon points='900 0 810 150 990 150'/%3E%3Cpolygon fill='%23222' points='0 300 -90 450 90 450'/%3E%3Cpolygon fill='%23FFF' points='0 300 90 150 -90 150'/%3E%3Cpolygon fill='%23FFF' points='180 300 90 450 270 450'/%3E%3Cpolygon fill='%23666' points='180 300 270 150 90 150'/%3E%3Cpolygon fill='%23222' points='360 300 270 450 450 450'/%3E%3Cpolygon fill='%23FFF' points='360 300 450 150 270 150'/%3E%3Cpolygon fill='%23444' points='540 300 450 450 630 450'/%3E%3Cpolygon fill='%23222' points='540 300 630 150 450 150'/%3E%3Cpolygon fill='%23AAA' points='720 300 630 450 810 450'/%3E%3Cpolygon fill='%23666' points='720 300 810 150 630 150'/%3E%3Cpolygon fill='%23FFF' points='900 300 810 450 990 450'/%3E%3Cpolygon fill='%23999' points='900 300 990 150 810 150'/%3E%3Cpolygon points='0 600 -90 750 90 750'/%3E%3Cpolygon fill='%23666' points='0 600 90 450 -90 450'/%3E%3Cpolygon fill='%23AAA' points='180 600 90 750 270 750'/%3E%3Cpolygon fill='%23444' points='180 600 270 450 90 450'/%3E%3Cpolygon fill='%23444' points='360 600 270 750 450 750'/%3E%3Cpolygon fill='%23999' points='360 600 450 450 270 450'/%3E%3Cpolygon fill='%23666' points='540 600 630 450 450 450'/%3E%3Cpolygon fill='%23222' points='720 600 630 750 810 750'/%3E%3Cpolygon fill='%23FFF' points='900 600 810 750 990 750'/%3E%3Cpolygon fill='%23222' points='900 600 990 450 810 450'/%3E%3Cpolygon fill='%23DDD' points='0 900 90 750 -90 750'/%3E%3Cpolygon fill='%23444' points='180 900 270 750 90 750'/%3E%3Cpolygon fill='%23FFF' points='360 900 450 750 270 750'/%3E%3Cpolygon fill='%23AAA' points='540 900 630 750 450 750'/%3E%3Cpolygon fill='%23FFF' points='720 900 810 750 630 750'/%3E%3Cpolygon fill='%23222' points='900 900 990 750 810 750'/%3E%3Cpolygon fill='%23222' points='1080 300 990 450 1170 450'/%3E%3Cpolygon fill='%23FFF' points='1080 300 1170 150 990 150'/%3E%3Cpolygon points='1080 600 990 750 1170 750'/%3E%3Cpolygon fill='%23666' points='1080 600 1170 450 990 450'/%3E%3Cpolygon fill='%23DDD' points='1080 900 1170 750 990 750'/%3E%3C/g%3E%3C/svg%3E");
}

html {
    font-size: 62.5%;
}

.rating {
    display: flex;
    justify-content: center;
}

.card {
    padding: 15px;
    border: 1px solid #eee;
    box-shadow: rgba(0, 0, 0, 0.06) 0px 2px 4px, rgba(0, 0, 0, 0.05) 0px 0.5px 1px;
    transition: all .3s ease-in-out;
}

.card:hover {
    box-shadow: rgba(0, 0, 0, 0.22) 0px 19px 43px, rgba(0, 0, 0, 0.18) 0px 4px 11px;
    transform: translate3d(0px, -1px, 0px);
    margin-top: -1px;
}

main {
    margin-bottom: 2rem;
}

.rating {
    display: flex;
    justify-content: center;
}

Don't worry about the body-image. It's an SVG file, which will look good as a background image. Make sure after creating this, you import index.css in index.js.

import "./index.css";

The final index.js after doing this, will look something like this.

// Final index.js
import React, { useEffect, useState } from "react";
import axios from "axios";
import ReactStars from "react-stars";
import {
  Grid,
  makeStyles,
  CardContent,
  CardMedia,
  Button,
  CardActionArea,
  Card,
  Typography,
  CardActions,
} from "@material-ui/core";
import Modal from "react-bootstrap/Modal";
import "./index.css";

export default () => {
  const [status, setStatus] = useState("loading...");
  const [reviews, setReviews] = useState(null);
  useEffect(() => {
    if (status !== "loading...") return;
    axios("/api/get-reviews").then((result) => {
      if (result.status !== 200) {
        console.error("Error loading Reviews");
        console.error(result);
        return;
      }
      setReviews(result.data.reviews);
      setStatus("loaded");
    });
  }, [status]);
  const getAvatar = () => {
    const random = Math.floor(Math.random() * (reviews.length - 0 + 1) + 0);
    const imgUrl = `https://avatars.dicebear.com/api/human/${random}.svg?mood[]=happy`;
    return imgUrl;
  };
  const useStyles = makeStyles({
    gridContainer: {
      paddingLeft: "40px",
      paddingRight: "40px"
    }
  });
  const useStylesCards = makeStyles({
    root: {
      minWidth: 200
    },
    bullet: {
      display: "inline-block",
      margin: "0 2px",
      transform: "scale(0.8)"
    },
    title: {
      fontSize: 14
    },
    pos: {
      marginBottom: 12
    },
    content:{
      flexGrow: 1,
      align: "center"
    },
    media: {
      height: 70,
      paddingTop: '56.25%', // 16:9
    }
  });

  // For using css values in card components
  const classes = useStyles();
  const classesCards = useStylesCards();
  return (
    <>
      <div className="container1">
        <div>
          <Grid
            container
            spacing={4}
            className={classes.gridContainer}
            justify="center"
          >
            {reviews &&
              reviews.map((review, index) => (
                <Grid item xs={12} sm={6} md={4}>
                  <Card
                    className={classesCards.root}
                    className="card"
                    variant="outlined"
                  >
                    <CardActionArea>
                      <CardMedia
                        className={classesCards.media}
                        height="10"
                        image={getAvatar()}
                        title="FaunaDB Users"
                      />
                      <CardContent>
                        <Typography
                          className={classesCards.content}
                          gutterBottom
                          variant="h4"
                          component="h2"
                        >
                          {review.name}
                        </Typography>
                        <Typography
                          variant="h4"
                          color="textSecondary"
                          component="h2"
                        >
                          {review.text}
                        </Typography>
                        <Typography variant="h5" component="h2">
                          <ReactStars
                            className="rating"
                            count={review.rating}
                            size={24}
                            color1={"#ffd700"}
                            edit={false}
                            half={false}
                          />
                          <br />
                        </Typography>
                      </CardContent>
                    </CardActionArea>
                  </Card>
                </Grid>
              ))}
          </Grid>
        </div>
      </div>

    </>
  );
};

Screenshot (258)

Now that we have an incomplete working model of the application let's deploy it to Netlify. After deploying to Netlify, we can use netlify identity to authenticate users.

Deploying to Netlify

Now, we have our application working. But there is one issue our app is running locally, and anyone cannot access it. So we will deploy the app to netlify so anyone in the world can access our application. If you don't have an account on netlify, you can log in with GitHub. Before that, make sure you create a repository and push the code to GitHub.

Once you have your code in the GitHub repository, go to netlify dashboard and select a new site from git. Inkednetlify-new-site_LI

Create-a-new-site-Netlify(github)

repo-netlify-deploy)edit)

Select the repository where you pushed your code.

In the Build command add Build command and in Publish directory add
deploysite(edit) Now go to site setting and then to build and deploy and then to Environment. Add FAUNA_ADMIN_SECRET and your database key used in the .env file while building the backend. env(buildand deploy)(edit)

admin-secret(edit) After the site is deployed, it will be accessible from anywhere over the internet. Now let's add authentication to our app, so the users need to login/signup to add his/her reviews to our app. Remember we are only allowing authenticated users to write a review on our application.

Adding Authentication

Yes, we can add authentication functionality in our serverless application. We will be using Netlify Identity for our authentication. Netlify Identities gives us an easy way to authenticate users by making minimal changes to our codebase, and it is very easy to set up and get started with.

Go to your site/ netlify app in the Netlify dashboard and click on Identities and then click on Enable Identities. And you have enabled Identities for your application.

Screenshot (267)(edit)

But to integrate it with our application, we will need to install some libraries and add some code to make sure we only allow authenticated users to and reviews in our application. Install these packages:

npm install gatsby-plugin-netlify-identity react-netlify-identity-widget @reach/dialog @reach/tabs @reach/visually-hidden

Make sure you are in the root folder before installing these dependencies. Gatsby Configuration. Remember the gatsby config file we kept empty let's update it to use Netlify plugin for our application. Add this to the gatsby configuration file(gatsby.config.js).

// Gatsby Config
module.exports = {
    plugins: [
        {
          resolve: `gatsby-plugin-netlify-identity`,
          options: {
            url: `https://YOUR-SITE-NAME.netlify.app/`
          }
        }
    ]
}

Let's work with our index.js to configure the netlify identity authentication. Import the packages/dependencies we installed.

import IdentityModal, { useIdentityContext } from "react-netlify-identity-widget";
import "react-netlify-identity-widget/styles.css";

Lets import react-bootstrap to set up a Button we will require for our Log In and Modal we will need for Add Reviews.

import Button from 'react-bootstrap/Button';
import Modal from 'react-bootstrap/Modal';

The react-netlify-identity-widget will provide us with a modal widget to provide credentials and create an account. We need to use/capture the credentials and check if the user is authenticated. So if the user is authenticated, we will show him the Add Review button. To have this, functionality in our app we can do.

const identity = useIdentityContext();
const [dialog, setDialog] = useState(false);
const username =
  (identity && identity.user && identity.user.user_metadata && 
       identity.user.user_metadata.full_name) || "Untitled";
const isLoggedIn = identity && identity.isLoggedIn;

Let's create a Log IN button in our render method add.

{identity && identity.isLoggedIn ? (
        <div className="auth-btn-grp">
          <Button variant="outline-primary" onClick={handleShow}>
            Add Review
          </Button>{" "}
          <Button
            variant="outline-primary"
            className="login-btn"
            onClick={() => setDialog(true)}
          >
            {isLoggedIn ? `Hello ${username}, Log out here!` : "LOG IN"}
          </Button>
        </div>
      ) : (
        <div className="auth-btn-grp">
          <Button
            variant="outline-primary"
            className="login-btn"
            onClick={() => setDialog(true)}
          >
            {isLoggedIn ? `Hello ${username}, Log out here!` : "LOG IN"}
          </Button>
        </div>
      )}

Add this identity dialogue after our card render method(jsx).

<IdentityModal showDialog={dialog} onCloseDialog={() => setDialog(false)} />

Add this to index.css to style our Log IN button.

.btn {
    min-height: 30px;
    min-width: 80px;
}
.auth-btn-grp {
    margin-top: 1px;
    margin-right: 2px;
    float: right;
    position: absolute;
    top:0;
    right:0;
 }

 .add-review {
    color: #000000;
 }

 .create-form {
    display: flex;
    justify-content: center;
    flex-direction: column;
 }

That's is all we will need to setup authentication using Netlify Identity.

Let's create a Modal to accept Name, Reviews and Rating from authenticated users and call our create-review api.

Add this method after the Identity method above the render method.

// Add Review
const [show, setShow] = useState(false);
const [rating, setRating] = useState(4);
const [name, setName] = useState('');
const [text, setText] = useState('');
const handleClose = () => setShow(false);
const handleShow = () => setShow(true);

const ratingChanged = (newRating) => {
  setRating(newRating);
}
const nameChanged = evt => {
  const val = evt.target.value;
  setName(val);
}
const textChanged = evt => {
  const val = evt.target.value;
  setText(val);
}
const handleCreate = async event => {
  if(text === '') return;
  await axios.post('/api/create-review', { name, text, rating });
  const newList = reviews.concat({ name, text, rating });
  setReviews(newList);
  setShow(false);

Add Modal to render method after identity dialogue.

<Modal
          show={show}
          onHide={handleClose}
          animation={true}
          className="add-review"
        >
          <Modal.Header closeButton>
            <Modal.Title>Add Review</Modal.Title>
          </Modal.Header>
          <Modal.Body>
            <div className="create-form">
            <textarea 
                onChange={(evt) => nameChanged(evt)} 
                placeholder="Enter your name here" />
              <textarea 
                onChange={(evt) => textChanged(evt)} 
                placeholder="Enter your message here" />
              <br />
              <span>Rating:</span> {' '} 
              <ReactStars
                count={5}
                value={rating}
                onChange={ratingChanged}
                size={24}
                color2={'#ffd700'}
                half={false} />
              </div>
          </Modal.Body>
          <Modal.Footer>
            <Button variant="secondary" onClick={handleClose}>
              Cancel
            </Button>
            <Button variant="primary" onClick={(evt) => handleCreate(evt)}>Create</Button>
          </Modal.Footer>
      </Modal>
    </>

Finally, after doing all this, our index.js will look something like this.

// Final index.js with authentication using netlify identity
import React, { useEffect, useState } from "react";
import axios from "axios";
import ReactStars from "react-stars";
import {
  Grid,
  makeStyles,
  CardContent,
  CardMedia,
  CardActionArea,
  Card,
  Typography,
} from "@material-ui/core";
import IdentityModal, {
  useIdentityContext,
} from "react-netlify-identity-widget";
import "react-netlify-identity-widget/styles.css";
import "bootstrap/dist/css/bootstrap.min.css";
import Button from "react-bootstrap/Button";
import Modal from "react-bootstrap/Modal";
import "./index.css";

export default () => {
  const [status, setStatus] = useState("loading...");
  const [reviews, setReviews] = useState(null);
  useEffect(() => {
    if (status !== "loading...") return;
    axios("/api/get-reviews").then((result) => {
      if (result.status !== 200) {
        console.error("Error loading Reviews");
        console.error(result);
        return;
      }
      setReviews(result.data.reviews);
      setStatus("loaded");
    });
  }, [status]);
  const getAvatar = () => {
    const random = Math.floor(Math.random() * (reviews.length - 0 + 1) + 0);
    const imgUrl = `https://avatars.dicebear.com/api/human/${random}.svg?mood[]=happy`;
    return imgUrl;
  };

  const identity = useIdentityContext();
  const [dialog, setDialog] = useState(false);
  const username =
    (identity &&
      identity.user &&
      identity.user.user_metadata &&
      identity.user.user_metadata.full_name) ||
    "Untitled";
  const isLoggedIn = identity && identity.isLoggedIn;

  const useStyles = makeStyles({
    gridContainer: {
      paddingLeft: "40px",
      paddingRight: "40px",
    },
  });
  const useStylesCards = makeStyles({
    root: {
      maxWidth: 200,
    },
    bullet: {
      display: "inline-block",
      margin: "0 2px",
      transform: "scale(0.8)",
    },
    title: {
      fontSize: 14,
    },
    pos: {
      marginBottom: 12,
    },
    content: {
      flexGrow: 1,
      align: "center",
    },
    media: {
      height: 70,
      paddingTop: "56.25%", // 16:9
    },
  });
// Add Review
const [show, setShow] = useState(false);
const [rating, setRating] = useState(4);
const [name, setName] = useState('');
const [text, setText] = useState('');
const handleClose = () => setShow(false);
const handleShow = () => setShow(true);

const ratingChanged = (newRating) => {
  setRating(newRating);
}
const nameChanged = evt => {
  const val = evt.target.value;
  setName(val);
}
const textChanged = evt => {
  const val = evt.target.value;
  setText(val);
}
const handleCreate = async event => {
  if(text === '') return;
  await axios.post('/api/create-review', { name, text, rating });
  const newList = reviews.concat({ name, text, rating });
  setReviews(newList);
  setShow(false);
}
  // For using css values in card components
  const classes = useStyles();
  const classesCards = useStylesCards();
  return (
    <>
      {identity && identity.isLoggedIn ? (
        <div className="auth-btn-grp">
          <Button variant="outline-primary" onClick={handleShow}>
            Add Review
          </Button>{" "}
          <Button
            variant="outline-primary"
            className="login-btn"
            onClick={() => setDialog(true)}
          >
            {isLoggedIn ? `Hello ${username}, Log out here!` : "LOG IN"}
          </Button>
        </div>
      ) : (
        <div className="auth-btn-grp">
          <Button
            variant="outline-primary"
            className="login-btn"
            onClick={() => setDialog(true)}
          >
            {isLoggedIn ? `Hello ${username}, Log out here!` : "LOG IN"}
          </Button>
        </div>
      )}
      <div className="container">
        <div>
          <Grid
            container
            spacing={4}
            className={classes.gridContainer}
            justify="center"
          >
            {reviews &&
              reviews.map((review, index) => (
                <Grid item xs={12} sm={6} md={4}>
                  <Card
                    className={classesCards.root}
                    className="card"
                    variant="outlined"
                  >
                    <CardActionArea>
                      <CardMedia
                        className={classesCards.media}
                        height="10"
                        image={getAvatar()}
                        title="FaunaDB Users"
                      />
                      <CardContent>
                        <Typography
                          className={classesCards.content}
                          gutterBottom
                          variant="h4"
                          component="h2"
                        >
                          {review.name}
                        </Typography>
                        <Typography
                          variant="h4"
                          color="textSecondary"
                          component="h2"
                        >
                          {review.text}
                        </Typography>
                        <Typography variant="h5" component="h2">
                          <ReactStars
                            className="rating"
                            count={review.rating}
                            size={24}
                            color1={"#ffd700"}
                            edit={false}
                            half={false}
                          />
                          <br />
                        </Typography>
                      </CardContent>
                    </CardActionArea>
                  </Card>
                </Grid>
              ))}
          </Grid>
        </div>
      </div>
      <IdentityModal showDialog={dialog} onCloseDialog={() => setDialog(false)} />
      <Modal
          show={show}
          onHide={handleClose}
          animation={true}
          className="add-review"
        >
          <Modal.Header closeButton>
            <Modal.Title>Add Review</Modal.Title>
          </Modal.Header>
          <Modal.Body>
            <div className="create-form">
            <textarea 
                onChange={(evt) => nameChanged(evt)} 
                placeholder="Enter your name here" />
              <textarea 
                onChange={(evt) => textChanged(evt)} 
                placeholder="Enter your message here" />
              <br />
              <span>Rating:</span> {' '} 
              <ReactStars
                count={5}
                value={rating}
                onChange={ratingChanged}
                size={24}
                color2={'#ffd700'}
                half={false} />
              </div>
          </Modal.Body>
          <Modal.Footer>
            <Button variant="secondary" onClick={handleClose}>
              Cancel
            </Button>
            <Button variant="primary" onClick={(evt) => handleCreate(evt)}>Create</Button>
          </Modal.Footer>
      </Modal>
    </>
  );
};

Our application will finally look like this.

Screenshot (272)

Conclusion

Till this point, we have to build a Serverless Fullstack application which uses lambda functions as a backend to communicated with our serverless database(Fauna). The framework for writing this function was similar as we write AWS Lambda function and we deployed it using Netlify Functions. We build a React frontend using Gatsby to make it look more attractive. And at last, we added authentication to our app using Netlify Identity. I hope this article clears all these concepts and make your next Serverless application building process more comfortable.

#serverless#jamstack#javascript#aws
 
Share this