Create a Bookmark Manager App using FaunaDB and Netlify Serverless functions
Tags: react, javascript, codenewbie, node
Published: 2020-09-24 03:52:11

### Introduction

In this article, you will create a Bookmark Manager App using FaunaDB GraphQL API and Netlify serverless functions. 

By creating this app with FaunaDB, you will understand how FaunaDB helps to quickly create GraphQL apps without worrying about managing the GraphQL server and its configuration on your own.

Fauna comes with GraphQL playground so you just have to provide the schema and Fauna does all the magic behind the scenes. 

You can see the live demo of the final working application in the video below <br />

{% youtube M8-feQTPpGY %}

<br />
- If you’re new to GraphQL and Apollo Client, check out my previous article [here](https://levelup.gitconnected.com/learn-the-basics-of-graphql-4740d76d5e8a?source=friends_link&sk=e9880f30dcce3dcaf2d555ecfc5d5e11) to understand the basics of GraphQL.
- If you're new to Serverless functions(lambda functions) check out my previous article [here](https://levelup.gitconnected.com/an-introduction-to-serverless-aws-lambda-functions-f2397e4e269e?source=friends_link&sk=9942fb13d09661bb66096bfbcfaccfce)

## FaunaDB GraphQL Configuration

- Login to FaunaDB with your GitHub / Netlify account or email and password.
- Once logged In, create a new database by clicking on the `NEW DATABASE` button

![Create Database](https://gist.github.com/myogeshchavan97/e0be7fc4c838544e2d00afeb3a82ae10/raw/b2579f62f7e760d71a4e5dd470b4fcd0cd515514/create_database.png)

- Enter the name of the database and click on the `SAVE` button

![New Database](https://gist.github.com/myogeshchavan97/e0be7fc4c838544e2d00afeb3a82ae10/raw/b2579f62f7e760d71a4e5dd470b4fcd0cd515514/new_database.png)

- You will see the following screen

![Database Created](https://gist.github.com/myogeshchavan97/e0be7fc4c838544e2d00afeb3a82ae10/raw/b2579f62f7e760d71a4e5dd470b4fcd0cd515514/db_created.png)

- Click on the  `GRAPHQL` menu displayed at the second last position

![GraphQL Screen](https://gist.github.com/myogeshchavan97/e0be7fc4c838544e2d00afeb3a82ae10/raw/b2579f62f7e760d71a4e5dd470b4fcd0cd515514/graphql_screen.png)

- Create a new file with the name `bookmarks.graphql` on your desktop and add the following code inside it:

```js
type Bookmark {
  title: String!
  url: String!
  tag: String!
}
type Query {
  bookmarks: [Bookmark!]!
}
```

- Now, click on the `IMPORT SCHEMA`  button shown in the above screenshot and select the `bookmarks.graphql` file

![Create Schema](https://gist.github.com/myogeshchavan97/e0be7fc4c838544e2d00afeb3a82ae10/raw/b2579f62f7e760d71a4e5dd470b4fcd0cd515514/create_schema.gif)

> As you can see, Fauna instantly creates queries and mutations for you and gives you a nice GraphQL playground where you can fire your queries and mutations for performing CRUD(Create, Read, Update and Delete) operations. Fauna also stores all the data in its own database so you don’t have to connect to other databases for your application. 

- Now, click on `SECURITY` menu which is just below the `GRAPHQL` menu and click on the `NEW KEY` button to create a secret key which you can use to make API requests to the FaunaDB

![New Key](https://gist.github.com/myogeshchavan97/e0be7fc4c838544e2d00afeb3a82ae10/raw/b2579f62f7e760d71a4e5dd470b4fcd0cd515514/new_key.png)

- Enter the name you want to give for the secret key and select `Server` for the `Role` dropdown value and click on the `SAVE` button

![Create Secret key](https://gist.github.com/myogeshchavan97/e0be7fc4c838544e2d00afeb3a82ae10/raw/b2579f62f7e760d71a4e5dd470b4fcd0cd515514/create_secret_key.png)

- Take note of your generated secret key as it will not be displayed again and keep it safe.

![Generated Secret Key](https://gist.github.com/myogeshchavan97/e0be7fc4c838544e2d00afeb3a82ae10/raw/b2579f62f7e760d71a4e5dd470b4fcd0cd515514/generated_secret_key.png)


> PS: Don’t use the secret key displayed in the screenshot above, I have regenerated a new key so the above key will not work 😊. Also don’t share your secret key with anyone as  anyone will mess up with your data

Now, let’s start writing code for our bookmark manager app.

## Initial Setup

Create a new project using `create-react-app`:

```js
create-react-app bookmark-manager
```

Once the project is created, delete all files from the `src` folder and create `index.js` and `styles.scss` files inside the `src` folder. Also create `actions`,  `components`, `custom-hooks` , `reducers`,  `router`,  `store` and `utils` folders inside the `src` folder.

Install the necessary dependencies:

```js
yarn add @apollo/client@3.1.4 apollo-boost@0.4.9 axios@0.20.0 bootstrap@4.5.2 cross-fetch@3.0.5 dotenv@8.2.0 graphql@15.3.0 lodash@4.17.20 node-sass@4.14.1 react-bootstrap@1.3.0 redux@4.0.5 react-redux@7.2.1 react-router-dom@5.2.0 redux-thunk@2.3.0 subscriptions-transport-ws@0.9.18 uuid@8.3.0
```

Open `styles.scss` and add the contents from [here](https://github.com/myogeshchavan97/bookmark-manager/blob/master/src/styles.scss) inside it.

## Writing code

Create a new file Header.js inside the components folder with the following content:

```js
import React from 'react';
import { Link } from 'react-router-dom';

const Header = () => {
  return (
    <header className="header">
      <h1 className="main-heading">Bookmark Manager</h1>
      <div className="header-links">
        <Link to="/add" className="link">
          Add Bookmark
        </Link>
        <Link to="/" className="link">
          Bookmarks List
        </Link>
      </div>
    </header>
  );
};

export default Header;
```

Create a new file `BookmarkSearch.js` inside the `components` folder with the following content:


```js
import React, { useState } from 'react';
import { Form } from 'react-bootstrap';

const BookmarkSearch = ({ handleSearch }) => {
  const [searchTerm, setSearchTerm] = useState('');

  const handleInputChange = (event) => {
    const value = event.target.value;
    setSearchTerm(value);
    handleSearch(value);
  };

  return (
    <div className="bookmark-search">
      <Form>
        <Form.Group controlId="location">
          <Form.Control
            type="text"
            name="searchTerm"
            className="searchTerm"
            value={searchTerm || ''}
            placeholder="Search by title or url"
            onChange={handleInputChange}
            autoComplete="off"
          />
        </Form.Group>
      </Form>
    </div>
  );
};

export default BookmarkSearch;
```

In this file, we have added an input search box for searching through the list of bookmarks.

Create a new file  `constants.js` inside the `utils` folder with the following content:

```js
export const SET_BOOKMARKS = 'SET_BOOKMARKS';
export const ADD_BOOKMARK = 'ADD_BOOKMARK';
export const EDIT_BOOKMARK = 'EDIT_BOOKMARK';
export const DELETE_BOOKMARK = 'DELETE_BOOKMARK';
export const GET_ERRORS = 'GET_ERRORS';
export const TAGS = [
  'All',
  'React',
  'Node.js',
  'JavaScript',
  'Beginners',
  'Other'
];
```

In this file, we have created constants to be used in redux and a set of tags in which we can group each bookmark.

Create a new file  `Filters.js` inside the `components` folder with the following content:

```js
import React from 'react';
import { TAGS } from '../utils/constants';

const Filters = ({ activeFilter, filterResults, handleFilterClick }) => {
  const handleClick = (tag) => {
    filterResults(tag);
    handleFilterClick(tag);
  };

  return (
    <div className="filters-list">
      <div className="filters">
        {TAGS.map((tag, index) => (
          <div
            key={index}
            onClick={() => handleClick(tag)}
            className={activeFilter === tag ? 'active' : ''}
          >
            {tag}
          </div>
        ))}
      </div>
    </div>
  );
};

export default Filters;
```

In this file, we’re looping over the list of tags we added in the `constant.js` file and displaying it on the screen.

Create a new file `Loader.js` inside the `components` folder with the following content:

```js
import { useState, useEffect } from 'react';
import ReactDOM from 'react-dom';

const Loader = (props) => {
  const [node] = useState(document.createElement('div'));
  const loader = document.querySelector('#loader');

  useEffect(() => {
    loader.appendChild(node).classList.add('message');
  }, [loader, node]);

  useEffect(() => {
    if (props.show) {
      loader.classList.remove('hide');
      document.body.classList.add('loader-open');
    } else {
      loader.classList.add('hide');
      document.body.classList.remove('loader-open');
    }
  }, [loader, props.show]);

  return ReactDOM.createPortal(props.children, node);
};

export default Loader;
```

In this file, we have created a loader component that will display a loading message with background overlay. 

To add it to the DOM, open `public/index.html` file and after the div with id `root` add another div with id `loader`

```js
<div id="root"></div>
<div id="loader"></div>
```

Create a new file `BookmarkItem.js` inside the `components` folder with the following content:

```js
import React from 'react';
import { Button } from 'react-bootstrap';

const BookmarkItem = ({ _id, title, url, tag, handleEdit, handleDelete }) => {
  return (
    <div className="bookmark">
      <div>
        <div className="title">
          <strong>Title: </strong>
          {title}
        </div>
        <div className="url">
          <strong>URL: </strong>
          {url}
        </div>
        <div className="tag">
          <strong>Tag: </strong>
          {tag}
        </div>
      </div>
      <div className="buttons">
        <div className="btn">
          <Button
            variant="info"
            type="submit"
            size="sm"
            onClick={() => handleEdit(_id)}
          >
            Edit
          </Button>
        </div>
        <div className="btn">
          <Button
            variant="danger"
            type="submit"
            size="sm"
            onClick={() => handleDelete(_id, title)}
          >
            Delete
          </Button>
        </div>
      </div>
    </div>
  );
};
export default BookmarkItem;
```

In this file, we’re displaying individual bookmarks with `edit` and `delete` buttons.

Create a new file `BookmarkList.js` inside the `components` folder with the following content:

```js
import React from 'react';
import BookmarkItem from './BookmarkItem';

const BookmarkList = ({ bookmarks, handleEdit, handleDelete }) => {
  return (
    <div className="bookmarks-list">
      {bookmarks.map((bookmark) => (
        <BookmarkItem
          key={bookmark._id}
          {...bookmark}
          handleEdit={handleEdit}
          handleDelete={handleDelete}
        />
      ))}
    </div>
  );
};

export default BookmarkList;
```

In this file, we’re iterating through the list of bookmarks and displaying it on the screen.

Create a new file `useLoader.js` inside the `custom-hooks` folder with the following content:

```js
import { useState } from 'react';

const useLoader = () => {
  const [isLoading, setIsLoading] = useState(false);

  const showLoader = () => {
    setIsLoading(true);
  };

  const hideLoader = () => {
    setIsLoading(false);
  };

  return { isLoading, showLoader, hideLoader };
};

export default useLoader;
```

In this file, we have separated out the showing and hiding loader into a custom hook.

Create a new file `BookmarkForm.js` inside the `components` folder with the following content:

```js
import React, { useState } from 'react';
import { Form, Button } from 'react-bootstrap';
import { TAGS } from '../utils/constants';

const BookmarkForm = (props) => {
  const [state, setState] = useState({
    title: props.title ? props.title : '',
    url: props.url ? props.url : '',
    tag: props.tag ? props.tag : 'React',
    tags: TAGS,
    errorMsg: ''
  });

  const onInputChange = (event) => {
    const { name, value } = event.target;
    setState((prevState) => ({
      ...prevState,
      [name]: value
    }));
  };

  const onFormSubmit = (event) => {
    event.preventDefault();
    const { title, url, tag } = state;
    const { _id } = props;
    const isEditPage = !!props.title;
    if (title.trim() !== '' && url.trim() !== '' && tag.trim() !== '') {
      let data = { title, url, tag };
      if (isEditPage) {
        data = { ...data, _id };
      }
      props.onSubmit(data);
    } else {
      setState((prevState) => ({
        ...prevState,
        errorMsg: 'Please fill out all the fields.'
      }));
    }
  };

  const { title, url, tags, tag, errorMsg } = state;
  return (
    <form onSubmit={onFormSubmit}>
      {errorMsg && <p className="errorMsg">{errorMsg}</p>}
      <Form.Group controlId="title">
        <Form.Label>Title</Form.Label>
        <Form.Control
          type="text"
          name="title"
          value={title || ''}
          onChange={onInputChange}
        />
      </Form.Group>
      <Form.Group controlId="description">
        <Form.Label>URL</Form.Label>
        <Form.Control
          type="text"
          name="url"
          value={url || ''}
          onChange={onInputChange}
        />
      </Form.Group>
      <Form.Group controlId="amount">
        <Form.Label>Tag</Form.Label>
        <Form.Control
          as="select"
          name="tag"
          value={tag || ''}
          onChange={onInputChange}
        >
          {tags.map((tag, index) => (
            <option key={index}>{tag}</option>
          ))}
        </Form.Control>
      </Form.Group>
      <Button variant="info" type="submit">
        Submit
      </Button>
    </form>
  );
};

export default BookmarkForm;
```

In this file, we have created a form to add and edit bookmark functionality.

Create a new file `AddBookmark.js` inside the `components` folder with the following content:

```js
import React from 'react';
import { connect } from 'react-redux';
import BookmarkForm from './BookmarkForm';
import { initiateAddBookmark } from '../actions/bookmarks';
import Loader from './Loader';
import useLoader from '../custom-hooks/useLoader';

const AddBookmark = (props) => {
  const { isLoading, showLoader, hideLoader } = useLoader();

  const onSubmit = (bookmark) => {
    showLoader();
    props.dispatch(initiateAddBookmark(bookmark)).then(() => {
      hideLoader();
      props.history.push('/');
    });
  };

  return (
    <div>
      <Loader show={isLoading}>Loading...</Loader>
      <BookmarkForm {...props} onSubmit={onSubmit} />
    </div>
  );
};

export default connect()(AddBookmark);
```

In this file, we have added an `onSubmit` handler that will call the `initiateAddBookmark` function to add a bookmark to the FaunaDB. we will write the code for `initiateAddBookmark` soon in this article.

Create a new file `EditBookmark.js` inside the `router` folder with the following content:

```js
import React from 'react';
import { connect } from 'react-redux';
import _ from 'lodash';
import { Redirect } from 'react-router-dom';
import BookmarkForm from './BookmarkForm';
import { initiateEditBookmark } from '../actions/bookmarks';
import useLoader from '../custom-hooks/useLoader';
import Loader from './Loader';

const EditBookmark = (props) => {
  const { isLoading, showLoader, hideLoader } = useLoader();

  const onSubmit = (bookmark) => {
    showLoader();
    props.dispatch(initiateEditBookmark(bookmark)).then(() => {
      hideLoader();
      props.history.push('/');
    });
  };

  return (
    <div>
      {!_.isEmpty(props.bookmark) ? (
        <React.Fragment>
          <Loader show={isLoading}>Loading...</Loader>
          <BookmarkForm onSubmit={onSubmit} {...props} {...props.bookmark} />
        </React.Fragment>
      ) : (
        <Redirect to="/" />
      )}
    </div>
  );
};

const mapStateToProps = (state, props) => ({
  bookmark: state.bookmarks.find(
    (bookmark) => bookmark._id === props.match.params.id
  )
});

export default connect(mapStateToProps)(EditBookmark);
```

In this file, when the user submits the bookmark after editing it, we‘re calling the `initiateEditBookmark` function to update the bookmark in FaunaDB.

Create a new file `Home.js` inside the `components` folder with the following content:

```js
import React, { useState, useEffect } from 'react';
import { connect } from 'react-redux';
import {
  initiateGetBookmarks,
  initiateDeleteBookmark
} from '../actions/bookmarks';
import BookmarkList from './BookmarkList';
import BookmarkSearch from './BookmarkSearch';
import Filters from './Filters';
import Loader from './Loader';
import useLoader from '../custom-hooks/useLoader';
import { isMatch } from '../utils/functions';

const Home = ({ bookmarksList, errorMsg, dispatch, history }) => {
  const [bookmarks, setBookmarks] = useState([]);
  const [activeFilter, setActiveFilter] = useState('All');
  const { isLoading, showLoader, hideLoader } = useLoader();

  const getBookmarks = () => {
    showLoader();
    dispatch(initiateGetBookmarks())
      .then(() => {
        setBookmarks(bookmarksList);
        hideLoader();
      })
      .catch(() => hideLoader());
  };

  useEffect(() => {
    getBookmarks();
  }, []);

  useEffect(() => {
    setBookmarks(bookmarksList);
  }, [bookmarksList]);

  const handleEdit = (id) => {
    history.push(`/edit/${id}`);
  };

  const handleDelete = (id, title) => {
    const shouldDelete = window.confirm(
      `Are you sure you want to delete the bookmark with title ${title}?`
    );
    if (shouldDelete) {
      showLoader();
      dispatch(initiateDeleteBookmark(id))
        .then(() => {
          handleFilterClick('All');
          hideLoader();
        })
        .catch(() => hideLoader());
    }
  };

  const handleSearch = (searchTerm) => {
    if (searchTerm) {
      setBookmarks(
        bookmarksList.filter((bookmark) => {
          const isTagMatch = isMatch(bookmark.tag, activeFilter);
          if (activeFilter !== '' && activeFilter !== 'All' && !isTagMatch) {
            return false;
          }
          const isTitleMatch = isMatch(bookmark.title, searchTerm);
          const isURLMatch = isMatch(bookmark.url, searchTerm);
          if (isTitleMatch || isURLMatch) {
            return true;
          }
          return false;
        })
      );
    } else {
      if (activeFilter !== 'All') {
        setBookmarks(
          bookmarksList.filter((bookmark) =>
            isMatch(bookmark.tag, activeFilter)
          )
        );
      } else {
        setBookmarks(bookmarksList);
      }
    }
  };

  const filterResults = (tag) => {
    if (tag !== 'All') {
      setBookmarks(bookmarksList.filter((bookmark) => bookmark.tag === tag));
    } else {
      setBookmarks(bookmarksList);
    }
  };

  const handleFilterClick = (tag) => {
    setActiveFilter(tag);
  };

  return (
    <React.Fragment>
      <BookmarkSearch handleSearch={handleSearch} />
      <Filters
        filterResults={filterResults}
        activeFilter={activeFilter}
        handleFilterClick={handleFilterClick}
      />
      {errorMsg && <p className="errorMsg">{errorMsg}</p>}
      <Loader show={isLoading}>Loading...</Loader>
      {bookmarks.length > 0 ? (
        <BookmarkList
          bookmarks={bookmarks}
          handleEdit={handleEdit}
          handleDelete={handleDelete}
        />
      ) : (
        <p className="no-result">No bookmarks found.</p>
      )}
    </React.Fragment>
  );
};

const mapStateToProps = (state) => ({
  bookmarksList: state.bookmarks,
  errorMsg: state.errorMsg
});

export default connect(mapStateToProps)(Home);
```

This is the main component file that encapsulated all other components.
In this file, first, we’re calling the `getBookmarks` function from `useEffect` hook by passing empty array as the second argument so the function will run only once.

```js
useEffect(() => {
 getBookmarks();
}, []);
```

Inside the `getBookmarks` function we’re setting the bookmarks array to the list of bookmarks returned using `setBookmarks(bookmarksList);`

If there is any update to the redux store either because the bookmark is added, edited, or deleted, then we’re taking that updated bookmarks and re-assigning it to the bookmarks array

```js
useEffect(() => {
  setBookmarks(bookmarksList);
}, [bookmarksList]);
```

This is similar to `componentDidUpdate` method of class where If there is any change in the `bookmarksList` prop(passed as a prop to the component from mapStateToProps), this useEffect will be executed.

Then inside the `handleEdit` method, we’re redirecting the user to the `EditBookmark` component by passing the edited bookmark id.

Inside the `handleDelete` method, we’re calling the `initiateDeleteBookmark` method to delete the bookmark once the user confirms the deletion.

Inside the `handleSearch` method,  we’re checking if the title or bookmark matches with the search term within a particular tag (activeFilter) from the list of bookmarks using the Array filter method and updating the bookmarks array based on the result.

Inside the `filterResults` method, we filter out the bookmarks based on which tag button is clicked.
 
Create a new file `AppRouter.js` inside the `router` folder with the following content:

```js
import React from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import Home from '../components/Home';
import AddBookmark from '../components/AddBookmark';
import EditBookmark from '../components/EditBookmark';
import BookmarkList from '../components/BookmarkList';
import Header from '../components/Header';

const AppRouter = () => (
  <BrowserRouter>
    <div className="container">
      <Header />
      <div className="bookmark-form">
        <Switch>
          <Route component={Home} path="/" exact={true} />
          <Route component={BookmarkList} path="/list" />
          <Route component={AddBookmark} path="/add" />
          <Route component={EditBookmark} path="/edit/:id" />
        </Switch>
      </div>
    </div>
  </BrowserRouter>
);

export default AppRouter;
```

Here, we have set up routing for various pages using `react-router-dom` library.

Create a new file `bookmarks.js` inside the `reducers` folder with the following content:

```js
import {
  SET_BOOKMARKS,
  ADD_BOOKMARK,
  EDIT_BOOKMARK,
  DELETE_BOOKMARK
} from '../utils/constants';

const bookmarksReducer = (state = [], action) => {
  switch (action.type) {
    case SET_BOOKMARKS:
      return action.bookmarks.reverse();
    case ADD_BOOKMARK:
      return [action.bookmark, ...state];
    case EDIT_BOOKMARK:
      return state.map((bookmark) => {
        if (bookmark._id === action._id) {
          return {
            ...bookmark,
            ...action.bookmark
          };
        } else {
          return bookmark;
        }
      });
    case DELETE_BOOKMARK:
      return state.filter((bookmark) => bookmark._id !== action._id);
    default:
      return state;
  }
};

export default bookmarksReducer;
```

In this reducer file, for the `SET_BOOKMARKS` action type, we’re returning the bookmarks in the reverse order so while displaying it on the UI, the latest added bookmark will be displayed at the top when the first time the component is loaded.

In the `ADD_BOOKMARK` action type, we’re returning the array by adding the newly added bookmark as the first item of the array and then using the spread operator, we’re appending all other bookmarks to the array.

In the `EDIT_BOOKMARK` action type, we’re checking if the passed id matches with any of the id from the bookmarks array using the array map method, and if it matches then we’re returning a new object by spreading out all the properties of the bookmark and then spreading out the updated values of the bookmark. 

For example, If the `bookmark` looks like this: 

```js
{_id: "276656761265455623221", title: "FaunaDB", url: "https://fauna.com/", tag: "React"}
```

and the `action.bookmark` looks like this:

```js
{_id: "276656761265455623221", title: "FaunaDB Website", url: "https://fauna.com/", tag: "React"}
```

where, only the title is changed then after using spread operator `{...bookmark, ...action.bookmark}` result will be:

```js
{_id: "276656761265455623221", title: "FaunaDB", url: "https://fauna.com/", tag: "React", _id: "276656761265455623221", title: "FaunaDB Website", url: "https://fauna.com/", tag: "React"}
```

and so If there is already key with the same name then the value of the later key will override the value of the former key. So the final result will be 

```js
{_id: "276656761265455623221", title: "FaunaDB Website", url: "https://fauna.com/", tag: "React"}
```
In the `DELETE_BOOKMARK` action type, we’re removing the bookmark with matching _id using the array filter method.

Create a new file `errors.js` inside the `reducers` folder with the following content:

```js
import { GET_ERRORS } from '../utils/constants';

const errorsReducer = (state = '', action) => {
  switch (action.type) {
    case GET_ERRORS:
      return action.errorMsg;
    default:
      return state;
  }
};

export default errorsReducer;
```

In this file, we are adding an error message coming from the FaunaDB if any while adding, editing, or deleting the bookmark.

Create a new file `store.js` inside the `store` folder with the following content:

```js
import { createStore, applyMiddleware, compose, combineReducers } from 'redux';
import thunk from 'redux-thunk';
import bookmarksReducer from '../reducers/bookmarks';
import errorsReducer from '../reducers/errors';

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
  combineReducers({
    bookmarks: bookmarksReducer,
    errorMsg: errorsReducer
  }),
  composeEnhancers(applyMiddleware(thunk))
);

store.subscribe(() => {
  console.log(store.getState());
});

export default store;
```

Here, we have created a redux store with `bookmarksReducer` and `errorsReducer` combined together so we can access store data from any component defined in the `AppRouter.js` file.

Create a new file `functions.js` inside the `utils` folder with the following content:

```js
export const isMatch = (original, search) =>
  original.toLowerCase().indexOf(search.toLowerCase()) > -1;
```

Now, open `src/index.js` file and add the following contents inside it:

```js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import AppRouter from './router/AppRouter';
import store from './store/store';
import 'bootstrap/dist/css/bootstrap.min.css';
import './styles.scss';

ReactDOM.render(
  <Provider store={store}>
    <AppRouter />
  </Provider>,
  document.getElementById('root')
);
```

Here, we have added a `Provider` component which will pass the redux store to all the Routes declared in the `AppRouter` component.

Create a new file `bookmarks.js` inside the `actions` folder with the following content:

```js
import axios from 'axios';
import {
  SET_BOOKMARKS,
  ADD_BOOKMARK,
  EDIT_BOOKMARK,
  DELETE_BOOKMARK
} from '../utils/constants';
import { getErrors } from './errors';

export const setBookmarks = (bookmarks) => ({
  type: SET_BOOKMARKS,
  bookmarks
});

export const addBookmark = (bookmark) => ({
  type: ADD_BOOKMARK,
  bookmark
});

export const editBookmark = (bookmark) => ({
  type: EDIT_BOOKMARK,
  _id: bookmark._id,
  bookmark
});

export const deleteBookmark = (_id) => ({
  type: DELETE_BOOKMARK,
  _id
});

export const initiateGetBookmarks = () => {
  return async (dispatch) => {
    try {
      const { data } = await axios({
        url: '/api/getBookmarks',
        method: 'POST'
      });
      return dispatch(setBookmarks(data));
    } catch (error) {
      error.response && dispatch(getErrors(error.response.data));
    }
  };
};

export const initiateAddBookmark = (bookmark) => {
  return async (dispatch) => {
    try {
      const { data } = await axios({
        url: '/api/addBookmark',
        method: 'POST',
        data: bookmark
      });
      return dispatch(addBookmark(data));
    } catch (error) {
      error.response && dispatch(getErrors(error.response.data));
    }
  };
};

export const initiateEditBookmark = (bookmark) => {
  return async (dispatch) => {
    try {
      const { data } = await axios({
        url: '/api/editBookmark',
        method: 'PUT',
        data: bookmark
      });
      return dispatch(editBookmark(data));
    } catch (error) {
      error.response && dispatch(getErrors(error.response.data));
    }
  };
};

export const initiateDeleteBookmark = (_id) => {
  return async (dispatch) => {
    try {
      const { data } = await axios({
        url: '/api/deleteBookmark',
        method: 'DELETE',
        data: { _id }
      });
      return dispatch(deleteBookmark(data._id));
    } catch (error) {
      error.response && dispatch(getErrors(error.response.data));
    }
  };
};
```

Create a new file `errors.js` inside the `actions` folder with the following content:

```js
import { GET_ERRORS } from '../utils/constants';

export const getErrors = (errorMsg) => ({
  type: GET_ERRORS,
  errorMsg
});
```

Create a new file `.env` inside the project folder with the following content:

```js
FAUNA_GRAPHQL_SECRET_KEY=your_fauna_secret_key
```

Use your faunaDB secret key here.

Open `.gitignore` file and add `.env` on the new line so the `.env` file will not be pushed to the git repository

Create a new file `netlify.toml` inside the project folder with the following content:

```js
[build]
  command="CI= yarn run build"
  publish="build"
  functions="functions"

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

This is the configuration file for Netlify where we specify the build configuration.
Let’s break it down

- The `command` specifies the command need to be executed to create a production build folder. The `CI=` is specific to Netify so netlify does not throw error while deploying the application.
- The `publish` specifies the name of the folder to be used for deploying the application
- The `functions` specifies the name of the folder where all our Serverless functions are stored
- All the serverless functions, when deployed to the Netlify, are available at the URL `/.netlify/functions/` so instead of specifying the complete path every time while making API call, we instruct Netlify that, whenever any request comes for `/api/function_name`, redirect it to `/.netlify/functions/function_name`. 
- `:splat`  specified that, whatever comes after `/api/` should be used after `/.netlify/functions`/

Create a `functions` folder in the root of your project inside which we will be writing our serverless functions.

Inside the` functions` folder, create a new `utils` folder and add the `bookmarks.graphql`  file with the following content:

```js
type Bookmark {
  title: String!
  url: String!
  tag: String!
}
type Query {
  bookmarks: [Bookmark!]!
}
```

Create a new file `client.js` inside the `functions/utils` folder with the following content:

```js
const { ApolloClient, InMemoryCache, HttpLink } = require('@apollo/client');
const { API_URL } = require('./constants');
const fetch = require('cross-fetch');
require('dotenv').config();

const getClient = ({ method = 'POST' } = {}) => {
  const client = new ApolloClient({
    link: new HttpLink({
      uri: API_URL,
      fetch,
      headers: {
        Authorization: `Bearer ${process.env.FAUNA_GRAPHQL_SECRET_KEY}`
      },
      method
    }),
    cache: new InMemoryCache()
  });
  return client;
};
module.exports = { getClient };
```

Create a new file `constants.js` inside the `functions/utils` folder with the following content:

```js
const API_URL = 'https://graphql.fauna.com/graphql';
const SET_BOOKMARKS = 'SET_BOOKMARKS';
const ADD_BOOKMARK = 'ADD_BOOKMARK';
const EDIT_BOOKMARK = 'EDIT_BOOKMARK';
const DELETE_BOOKMARK = 'DELETE_BOOKMARK';

module.exports = {
  API_URL,
  SET_BOOKMARKS,
  ADD_BOOKMARK,
  EDIT_BOOKMARK,
  DELETE_BOOKMARK
};
```

Note the `API_URL`  here, it’s the same URL that is displayed in the FaunaDB GraphQL playground which we’re using.

Create a new file `queries.js` inside the `functions/utils` folder with the following content:

```js
const { gql } = require('apollo-boost');

const GET_BOOKMARKS = gql`
  query {
    bookmarks {
      data {
        _id
        title
        url
        tag
      }
    }
  }
`;

const ADD_BOOKMARK = gql`
  mutation($title: String!, $url: String!, $tag: String!) {
    createBookmark(data: { title: $title, url: $url, tag: $tag }) {
      _id
      title
      url
      tag
    }
  }
`;

const EDIT_BOOKMARK = gql`
  mutation($id: ID!, $title: String!, $url: String!, $tag: String!) {
    updateBookmark(id: $id, data: { title: $title, url: $url, tag: $tag }) {
      _id
      title
      url
      tag
    }
  }
`;

const DELETE_BOOKMARK = gql`
  mutation($id: ID!) {
    deleteBookmark(id: $id) {
      _id
    }
  }
`;

module.exports = {
  GET_BOOKMARKS,
  ADD_BOOKMARK,
  EDIT_BOOKMARK,
  DELETE_BOOKMARK
};
```

Create a new file `getBookmarks.js` inside the `functions` folder with the following content:

```js
const { GET_BOOKMARKS } = require('./utils/queries');
const { getClient } = require('./utils/client');

exports.handler = async (event, context, callback) => {
  try {
    const client = getClient();
    let { data } = await client.query({
      query: GET_BOOKMARKS
    });
    const result = data.bookmarks.data;
    return {
      statusCode: 200,
      body: JSON.stringify(result)
    };
  } catch (error) {
    return {
      statusCode: 500,
      body: JSON.stringify(
        'Something went wrong while fetching bookmarks. Try again later.'
      )
    };
  }
};
```

In this file, we’re actually making an API call to the FaunaDB GraphQL API and returning the response to the `initiateGetBookmarks` function defined inside the `src/actions/bookmarks.js` file because from inside the `initiateGetBookmarks` function, we’re making a call to the `/api/getBookmarks` which is `functions/getBookmarks.js` serverless function.

## Running the Application

Now, let’s run the application to see the output. Before that, we need to install `netlify-cli` npm library which will run our serverless functions and also our React app.

Install the library by executing the following command from the terminal:

```js
npm install netlify-cli -g
```

If you’re on Linux / Mac then you might need to add `sudo` before it to install it globally:

```js
sudo npm install netlify-cli -g
```

Now, start the application by running the following command from the terminal from inside the project folder

```js
netlify dev
```

`netlify dev` command will first run our serverless functions and then our React application and it will automatically manage the proxy so you will not get CORS error while accessing the serverless functions from the React application.

Now, navigate to http://localhost:8888/ and check the application

![Initial Screen](https://gist.github.com/myogeshchavan97/e0be7fc4c838544e2d00afeb3a82ae10/raw/b2579f62f7e760d71a4e5dd470b4fcd0cd515514/initial_screen.png)

## Adding bookmarks

Currently, we have not added any bookmarks so the application is showing `No bookmarks found` message. So let’s add some bookmarks.

Create a new file `addBookmark.js` inside the `functions` folder with the following content:

```js
const { ADD_BOOKMARK } = require('./utils/queries');
const { getClient } = require('./utils/client');

exports.handler = async (event, context, callback) => {
  try {
    if (event.httpMethod !== 'POST') {
      return {
        statusCode: 405,
        body: JSON.stringify({
          error: 'only POST http method is allowed.'
        })
      };
    }
    const { title, url, tag } = JSON.parse(event.body);
    const variables = { title, url, tag };
    const client = getClient();
    const { data } = await client.mutate({
      mutation: ADD_BOOKMARK,
      variables
    });
    const result = data.createBookmark;
    return {
      statusCode: 200,
      body: JSON.stringify(result)
    };
  } catch (error) {
    return {
      statusCode: 500,
      body: JSON.stringify('Something went wrong. Try again later!')
    };
  }
};
```

Now, restart the server by running `netlify dev` again and add a bookmark by clicking on the `Add Bookmark` link in the header

![Add Bookmark](https://gist.github.com/myogeshchavan97/e0be7fc4c838544e2d00afeb3a82ae10/raw/b2579f62f7e760d71a4e5dd470b4fcd0cd515514/add_bookmark.gif)

## Adding Edit and Delete Bookmark functionality

Let’s add the edit and delete bookmark serverless functions now.

Create a new file `editBookmark.js` inside the `functions` folder with the following content:

```js
const { EDIT_BOOKMARK } = require('./utils/queries');
const { getClient } = require('./utils/client');

exports.handler = async (event, context, callback) => {
  try {
    if (event.httpMethod !== 'PUT') {
      return {
        statusCode: 405,
        body: JSON.stringify({
          error: 'only PUT http method is allowed.'
        })
      };
    }
    const { _id: id, title, url, tag } = JSON.parse(event.body);
    const variables = { id, title, url, tag };
    const client = getClient({ method: 'PUT' });
    const { data } = await client.mutate({
      mutation: EDIT_BOOKMARK,
      variables
    });
    const result = data.createBookmark;
    return {
      statusCode: 200,
      body: JSON.stringify(result)
    };
  } catch (error) {
    return {
      statusCode: 500,
      body: JSON.stringify(
        'Something went wrong while editing bookmarks. Try again later.'
      )
    };
  }
};
```

Create a new file `deleteBookmark.js` inside the `functions` folder with the following content:

```js
const { DELETE_BOOKMARK } = require('./utils/queries');
const { getClient } = require('./utils/client');

exports.handler = async (event, context, callback) => {
  try {
    if (event.httpMethod !== 'DELETE') {
      return {
        statusCode: 405,
        body: JSON.stringify({
          error: 'only DELETE http method is allowed.'
        })
      };
    }
    const { _id: id } = JSON.parse(event.body);
    const variables = { id };
    const client = getClient({ method: 'DELETE' });
    const { data } = await client.mutate({
      mutation: DELETE_BOOKMARK,
      variables
    });
    const result = data.deleteBookmark;
    return {
      statusCode: 200,
      body: JSON.stringify(result)
    };
  } catch (error) {
    return {
      statusCode: 500,
      body: JSON.stringify(
        'Something went wrong while deleting bookmark. Try again later.'
      )
    };
  }
};
```

Now, restart the server by running `netlify dev` again and check the edit and delete bookmark functionality.

### Edit bookmark functionality

![Edit Bookmark](https://gist.github.com/myogeshchavan97/e0be7fc4c838544e2d00afeb3a82ae10/raw/b2579f62f7e760d71a4e5dd470b4fcd0cd515514/edit_bookmark.gif)

### Delete bookmark functionality

![Delete Bookmark](https://gist.github.com/myogeshchavan97/e0be7fc4c838544e2d00afeb3a82ae10/raw/b2579f62f7e760d71a4e5dd470b4fcd0cd515514/delete_bookmark.gif)

Let’s add a couple of more bookmarks in various tags.

![Bookmarks](https://gist.github.com/myogeshchavan97/e0be7fc4c838544e2d00afeb3a82ae10/raw/b2579f62f7e760d71a4e5dd470b4fcd0cd515514/bookmarks.gif)

Now, we have added some bookmarks, Let’s verify the search bookmarks functionality.

![Working Search](https://gist.github.com/myogeshchavan97/e0be7fc4c838544e2d00afeb3a82ae10/raw/b2579f62f7e760d71a4e5dd470b4fcd0cd515514/working_search.gif)

## Testing the data from FaunaDB GraphQL Playground

Let’s verify that they are actually added to the FaunaDB.

Navigate to the GraphQL menu from the FaunaDB dashboard and paste the query for getting all bookmarks from `functions/utils/queries.js` file into the playground and verify it.

```js
query {
  bookmarks {
    data {
      _id
      title
      url
      tag
    }
  }
}
```

![Verify Get Bookmarks](https://gist.github.com/myogeshchavan97/e0be7fc4c838544e2d00afeb3a82ae10/raw/b2579f62f7e760d71a4e5dd470b4fcd0cd515514/verify_get_bookmarks.gif)

As you can see, the bookmarks are correctly saved into the FaunaDB, so now our bookmarks will persist even after refreshing the page.

Let’s recap on how the app works.


- When the app is loaded, we’re calling `initiateGetBookmarks` function of `actions/bookmarks.js` file, from `components/Home.js` file.
- The `initiateGetBookmarks` function, makes an API call to the`/api/getBookmarks`  URL which is a serverless function written in `functions/getBookmarks.js` file which finally calls the FaunaDB GraphQL API for getting the list of Bookmarks.
- When we edit/delete the bookmark, respective serverless functions are called from `functions` folder making an API call to FaunaDB.

## Deploy the application to Netlify

Now, we're done with the application. 

To deploy the application to Netlify follow any of your favorite way from [this article](https://medium.com/javascript-in-plain-english/deploy-your-react-application-to-production-within-seconds-58754c79c239?source=friends_link&sk=040204faa8a15e6ae2b7b2e70abe130a)

> Make sure to add `FAUNA_GRAPHQL_SECRET_KEY` environment variable with your fauna secret key to `Site settings => Build & deploy => Environment Variables` section in Netlify and re-deploy the site.

## Conclusion

As you have seen, FaunDB makes it really easy to create a GraphQL server and store the data in the database so we don’t have to worry about using an extra database for storing the data.

We’re done with creating our amazing Bookmark Manager JAMStack App using blazing-fast FaunaDB GraphQL API and Netlify.

You can find the complete source code for this application [in this repository](https://github.com/myogeshchavan97/bookmark-manager)

**Don't forget to subscribe to get my weekly newsletter with amazing tips, tricks and articles directly in your inbox [here](https://yogeshchavan.dev).**

[<img src="https://cdn.buymeacoffee.com/buttons/default-yellow.png" >](https://www.buymeacoffee.com/myogeshchavan97)