Build a basic social graph with Fauna and Next.js
Tags: fauna
Published: 2021-04-29 14:23:35

---
title: Build a basic social graph with Fauna and Next.js
published: true
description: 
tags: fauna
cover_image: https://images.unsplash.com/photo-1601457286963-e49a2f14eaf2?ixlib=rb-1.2.1&q=85&fm=jpg&crop=entropy&cs=srgb&w=3600
---
Fauna is a next-generation cloud database that combines the simplicity of NoSQL, without sacrificing the ability to model complex relationships. It’s completely serverless, fast, ACID-compliant, and has a generous free tier for small apps - basically everything you could possibly want in a fully-managed database.

This tutorial demonstrates how to model a basic social graph with Fauna and access it on the web with Next.js. We would build out some of the functions a small social network requires to work while introducing you to **[Fauna Query Language](https://docs.fauna.com/fauna/current/api/fql/cheat_sheet)** (FQL), the common patterns for reading and writing to the database and everything you need to get up and running with Fauna.

Feel free to make refrence to the [source code](https://github.com/Robogeek95/social_graph.git) as we progress

Excited Already? when you're ready, let's hop in...

# Fauna Setup

To get started, go to the Fauna Dashboard and sign up for a free Fauna account if you do not already have an account. Once you are in the Dashboard, click New Database, enter a name for the database, and Save. You should now be on the "Overview" page of your new database.

## **Database Structure**

We would create some collections to work with. A collection is like an SQL table, and works very similar to other document-oriented databases like MongoDB, just view a collection as a folder that has many files, then you can run a query against the folder to filter out the files you need.

To create a collection, go to the Fauna Dashboard and click on `collections`, then click on `new collection`, name your collection(usually in a plural form) and save it.

You would notice the TTL(time to live) option which automatically deletes the data you no longer need this type of data is also known as ephemeral data

You might also notice the History option, this is another unique feature of Fauna. Fauna never updates a stored document directly but instead, it creates a copy of the original document with the changes, then it archives the original document. This is a cool feature that could come in handy for data changes time travelling.

### Create collections

The database contains three collections - **`users`**, **`posts`**, and **`followers`**. Go ahead and create these collections from the Fauna dashboard.

![Screenshot_from_2021-04-25_04-57-49](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vmsdrj5p52nbri3fzrt2.png)
 

## Setting-up: Following relationships

It’s time to add the *following* relationships between our users. In this tutorial, we will use a *follower/followee* relationship in which a user can follow another user, this is not necessarily reciprocal.

From [Fauna's dashboard](https://dashboard.fauna.com/accounts/register?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_SocialMediaDatabase_SJamie) create a new collection called followers*.* You would also create three new indexes for followers collection: `followers_by_followee` returns all the followers of a user, the `followees_by_follower` index will return the people a user is following (followees). The third index called `is_followee_of_user`, we would use this index to determine if a user is already following another user and make unique the document related to the following condition.

### Create Index *`followers_by_followee`*

![fauna-index-followers-by-followee_lQusBDtoC](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xn77ji1k5gmlkuyafcn7.png)
 6f/fauna-index-followers-by-followee_lQusBDtoC.png)

you can Create the from the Fauna’s shell using [CreateIndex](https://docs.fauna.com/fauna/current/api/fql/functions/createindex?lang=javascript) method:

```bash
CreateIndex({
  name: "followers_by_followee",
  unique: false,
  serialized: true,
  source: Collection("Followers"),
  terms: [
    {
      field: ["data", "followee"]
    }
  ],
  values: [
    {
      field: ["data", "follower"]
    }
  ]
})
```

### Create Index *`followees_by_follower`*
![fauna-index-followees-by-follower_1Bog9IVqk](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w28urrrq1uw2e9c85w22.png)
 
also with Fauna Shell:
 
```bash
CreateIndex({
  name: "followees_by_follower",
  unique: false,
  serialized: true,
  source: Collection("Followers"),
  terms: [
    {
      field: ["data", "follower"]
    }
  ],
  values: [
    {
      field: ["data", "followee"]
    }
  ]
})

```

### Create the Index *`is_followee_of_user`*

![Fauna-index-is-following-user_x1eGFrWh5](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r2n8gz9hdjq7kfqcygzm.png)

with the fauna shell:

```bash
CreateIndex({
  name: "is_followee_of_user",
  unique: true,
  serialized: true,
  source: Collection("followers"),
  terms: [
    {
      field: ["data", "follower"]
    },
    {
      field: ["data", "followee"]
    }
  ]
})
```

### Create index *`posts_by_owner`*.

This index is for the posts collection. It would have the field `owner` as term and the value field will be empty.

![fauna-index-post-by-owner_fpLE6O_WC](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yocockggk16ek66d601m.png)
 

To create this index with the fauna shell

```bash
CreateIndex({
  name: "posts_by_owner",
  unique: true,
  serialized: true,
  source: Collection("posts"),
  terms: [
    {
      field: ["data", "owner"]
    }
  ]
})
```

Now, our database can contain something more than users. 

With these, we have used indexes to prepare our database to handle follower/followee relationships between all users. The next step is to create some functions to authenticate users, follow users and to create posts

## User Defined Functions(UDF)

User-defined functions (UDF) are Fauna Query Language(FQL) sort of anonymous functions. UDFs are a great way to improve your site's performance, reduce redundancy in code, reduce latency on requests and promote a cleaner code.

[Fauna](https://dashboard.fauna.com/accounts/register?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_SocialMediaDatabase_SJamie) allows you to create your own functions using the FQL methods described [here](https://docs.fauna.com/fauna/current/api/fql/cheat_sheet), this tutorial would only be covering a few of these methods. I recommend keeping the cheatsheet close. 

On the Fauna dashboard, open the functions menu and click on `New Function`.

![fauna-new-function_TpAKX9HLt6](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h20ujdlz6ulncn3nbvrw.png)
 
You should see an example snippet Fauna provides by default and it should be similar to this

```
Query(
  Lambda(
    "x", Add(Var("x"), Var("x"))
  )
)

```

Let's Break it down:

- Query: it’s only parameter is a lambda function and its purpose is to prevent the lambda function for immediate execution. It encases the function definition.
- Lambda: this method has two parameters, the first one is the set of arguments the function can get (in this case, x), and the second one is the lambda function, which means the commands we will execute. All argument names should be strings, also, if you need to use more than one parameter, you should put all names in an array (e.g [“x”,”y”]).
- Add: In the example code provided by Fauna, they use `Add` as the only method used, this returns the arithmetic sum of all the arguments. However, we will change this part to log the user in.
- Var: This method is invoked whenever we make a reference to an existing variable, it accepts the name of the variable as a **string** as an **argument**.

### Signup User Function

Now that you get the picture, update the default function Fauna provided into a function for creating users, name our function `signupUser` and save it.

```bash
Query(
  Lambda(
    ["email","password"],
    Let(
      {
        user:Create(
          Collection("Users"),
          {
            credentials: { password: Var("password") },
            data: {
              email: Var("email"),
              posts: 0,
              activeSince: Now()
            }
        }),
       userRef: Select(
          "ref",
          Var("user")
        ),
      },
      Login(Var("userRef"), {
        password: Var("password"),
        data: {
          message: "logged in"
        }
      })
    )
  )
)

```

The parameters in the Lambda function demonstrates how to accept more than one variable. In this case, *email* is the user’s email and *password* is the user’s password. The method `Let` allows you to create an object with temporal variables (represented as the object’s fields) and use them in the second argument by calling the method `Var`. We create a field named *user* and define it as the response for creating a new user on the Users collection with the data provided and some additional fields (for reference). The response of `Create` is the created document.

We also create a field called *userRef* in which we Select the field *ref* of our newly created user. After defining our binding variables, we set the second parameter of `Let` to `Login` the user, this means, the `Let` method will return the result of Login. When you login a user, you can provide additional data, we did put a field called *message* and put the string *logged in*.

Right now the UDF doesn’t seem as much, but when we start expanding our database, we will have more efficiency by having UDFs instead of performing many database queries.

You can make these functions available in your repository by adding them to a setup file. This way, when you are setting up a similar database for another server, you can recreate the same structure with just a single command. But we would not cover that in this tutorial. You can adapt [this example](https://github.com/fauna-brecht/fwitter/blob/master/src/fauna/setup) from Fauna’s developer team.

### Follow users function

```bash
CreateFunction({
  name: ‘followUsers’
  role: null,
  body: Query(
    Lambda(
      "followee",
      If(
        IsEmpty(
          Match(Index("is_followee_of_user"), [Identity(), Var("followee")])
        ),
        Do(
          Create(Collection("Followers"), {
            data: { follower: Identity(), followee: Var("followee") }
          }),
          { isFollowing: true }
        ),
        Do(
          Delete(
            Select(
              ["data", 0],
              Paginate(
                Match(Index("is_followee_of_user"), [
                  Identity(),
                  Var("followee")
                ])
              )
            )
          ),
          { isFollowing: false }
        )
      )
    )
  )
})

```

This function toggles the follow/unfollow state of the users. If you already follow a user, you’ll stop following the user, if you are not a follower, you’ll become one. Also, this function returns the new following status as `true` or `false`.

### Create Post Function

```bash
CreateFunction({
  name: "createPost",
  role: null,
  body: Query(
    Lambda(
      "description",
      Create(Collection("Posts"), {
        data: {
          description: Var("description"),7 I'll
          date: Now(),
          owner: Identity()
        }
      })
    )
  )
})

```

With this function, you can create a new post and put initial values like the date it was posted as well as set the number of likes and comments to 0.

### List Accounts Function

```bash
CreateFunction({
  name: "listAccounts",
  role: null,
  body: Query(
    Lambda(
      "cursor",
      Map(
        Paginate(Reverse(Documents(Collection("Users"))), {
          after: Var("cursor")
        }),
        Lambda("ref", {
          userId: Select("id", Var("ref")),
          isFollowee: IsNonEmpty(
            Match(Index("is_followee_of_user"), [Identity(), Var("ref")])
          ),
        isSelf: Equals(Identity(), Var("ref"))
        })
      )
    )
  )
})

```

This function returns all the user accounts, Paginate will return a page of 64 documents by default on every call, to fetch the next page of 64 users, we can send a *cursor* variable containing the ref of the last user from the last result. Also, we can change the size of every page as we need. The response will contain a field called *data* which is an array of objects containing the fields *userId* (a string with the reference of the user), *isFollowee* (a boolean stating if you are following this user), and *isSelf* (a boolean indicating whether this user is you).

### Get Feed Function

```bash
CreateFunction({
  name: "getFeed",
  role: null,
  body: Query(
    Lambda(
      "cursor",
      Map(
        Paginate(Reverse(Documents(Collection("Users"))), {
          after: Var("cursor")
        }),
        Lambda("ref", {
          userId: Select("id", Var("ref")),
          isFollowee: IsNonEmpty(
            Match(Index("is_followee_of_user"), [Identity(), Var("ref")])
          ),
        isSelf: Equals(Identity(), Var("ref"))
        })
      )
    )
  )
})
```

We’ve got several functions and indexes, but our users have permissions to none of them, all they can do is get their own user id. Let’s use the Fauna dashboard and the hints they provide to help us set the permissions for everyone.

## Manage Roles

We’ve got several functions and indexes, but our users have permissions to none of them, First, let’s navigate to the **manage roles** section:

![Fauna-new-role_4XXsEWhkE](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7640shbe3gizebev9on0.png)
 
 

Click on **new custom role** and name it *basicUser*, then start adding the collections and functions, add everything except the index called *users_by_email* and the function called *signupUser*.

![Fauna-new-role-2_wTuql__Bq](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/35gpkd2mkolryhpblo1l.png)
 

Fauna’s ABAC (Atribute-Based Access Control) will grant the documents of a collection all the permits that you grant. An authenticated document (in this case user) can have one or more roles, if any role grants permission to perform a certain action, the action will be performed when required.

After you finish adding your collections and indexes, you should see this:

![Fauna-new-role-3_6fRu5wJEK](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/41itmmqto2cyxd4dl9pu.png)
 

Each row represents a collection, index or function. Each column stands for an action.+Over the Collections your actions are *Read* / *Write*(update) / *Create* / *Delete* / *Read History* / *Write on History* / *Unrestricted* (do all)+Over the indexes, you can *Read* / *Unrestricted* access (read all index's records, even for documents you can’t directly access)+You can *Call* functions

Now, let’s grant the permissions for these items, click on the red X to turn it into a green checkmark on the next items:+*Read* permissions for collections *Users, Followers, Posts*.+*Create* permissions on *Followers* and *Posts* (we will change that later).+*Delete* permissions on *Followers*.+*Read* permissions on all indexes.+*Call* permissions on all functions.

Finally, let’s click on the **Membership** tab on the upper side of the window to define who will have the permissions we’ve just set.

Select the *Users* collection as the users will be the ones with these permissions, now you can click on the new Row with the name Users to set a predicate function. In this case, all users will have permissions, so let’s just make the function return `true` all the time.

![Fauna-new-role-4_cWaCbEUQ7](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b4vbqb28dsn6wxx329cy.png)
 
Save it, at this point the database is fully prepared for our basic social graph. The next step is to wrap the client around it and start making actual interactions on the database.

# Next.js Setup

Next.js would enable us to harness the full power of the jamStack while using Fauna as the database. If you are not familiar with Next.js you should probably read up on it [here](https://nextjs.org/)

## Initialize Next.js

Start by creating a Next.js application

```bash
npx-create-nextapp social_fauna
```

The pages directory holds the actual UI code for the pages and the `pages/api` directory provides a solution to build your **API** directly with Next.js. We would place all the actula logics that ineracts with the api inside the api directory

we would be using bootstrap to quickly style our pages, so let's intstall the bootstrap npm package, i am using yarn but you can also use npm if you prefer.

Install the bootstrap package

```bash
yarn add bootstrap
```

import bootstrap css file in the custom _app.js file

```jsx
import 'bootstrap/dist/css/bootstrap.css'
```

### Store API key in env file

First, create a server key from the Fauna security tab

![fauna key](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e7nxnrdc3ls0ourcu9f0.png)
 
then create a .env file at the root level of your next.js project and store your key in this format

```jsx
FAUNA_SERVER_KEY = <your-server-key-here>
```

we're done setting up the next.js project, the next thing we need to do is to define some important auth modules utils.

### Fauna auth util

create file utils/fauna-auth.js

```jsx
import faunadb from 'faunadb'
import cookie from 'cookie'

export const FAUNA_SECRET_COOKIE = 'faunaSecret'

export const serverClient = new faunadb.Client({
  secret: process.env.FAUNA_SERVER_KEY,
})

console.log(process.env.FAUNA_SERVER_KEY)

// Used for any authed requests.
export const faunaClient = (secret) =>
  new faunadb.Client({
    secret,
  })

export const serializeFaunaCookie = (userSecret) => {
  const cookieSerialized = cookie.serialize(FAUNA_SECRET_COOKIE, userSecret, {
    sameSite: 'lax',
    secure: process.env.NODE_ENV === 'production',
    maxAge: 72576000,
    httpOnly: true,
    path: '/',
  })
  return cookieSerialized
}
```

this file provides the functions for authorizing requests to fauna

### Auth util

create file utils/auth.js

```jsx
import { useEffect } from 'react'
import Router from 'next/router'

export const login = ({ email }) => {
  Router.push('/feed')
}

export const logout = async () => {
  await fetch('/api/logout')

  window.localStorage.setItem('logout', Date.now())

  Router.push('/login')
}

export const withAuthSync = (Component) => {
  const Wrapper = (props) => {
    const syncLogout = (event) => {
      if (event.key === 'logout') {
        console.log('logged out from storage!')
        Router.push('/login')
      }
    }

    useEffect(() => {
      window.addEventListener('storage', syncLogout)

      return () => {
        window.removeEventListener('storage', syncLogout)
        window.localStorage.removeItem('logout')
      }
    }, [])

    return <Component {...props} />
  }

  return Wrapper
}
```

the login function, the logout function and a react hook to update the current auth state and ensure it's in sync.

now, update the index.js file to reflect this

```jsx
import { useRouter } from "next/router";
import { logout } from "../utils/auth";

const Home = () => {
  const router = useRouter();
  return (
    <div className="container">
      <div>
        <h1>Basic social graph example</h1>

        <p>Steps to test the features:</p>

        <ol>
          <li>
            Click signup and create an account, this will also log you in.
          </li>
          <li>
            Click home and click feed, notice how your session is being
            used through a token stored in a cookie.s
          </li>
          <li>
            Click logout and try to go to feed again. You'll get redirected
            to the `/login` route.
          </li>
        </ol>
        <div className="row mt-5">
          <div
            className="col-3 border p-4 rounded text-center"
            onClick={() => router.push("/signup")}
          >
            SignUp
          </div>
          <div
            className="col-3 border p-4 rounded text-center"
            onClick={() => router.push("/signin")}
          >
            Login
          </div>
          <div
            className="col-3 border p-4 rounded text-center"
            onClick={logout}
          >
            Logout
          </div>
          <div
            className="col-3 border p-4 rounded text-center"
            onClick={() => router.push("/feed")}
          >
            Feed
          </div>
        </div>
      </div>
      <style jsx>{`
        li {
          margin-bottom: 0.5rem;
        }
      `}</style>
    </div>
  );
};

export default Home;
```

i have added some key points, links and methods to help you test out the functionalities we would be implementing.

## User Signin

The user signs in from this page 

### Signin page

create a new file `/pages/signin.js` and duplicate this:

```jsx
import { useState } from "react";
import Router from "next/router";

function signin() {
  const [userData, setUserData] = useState({
    email: "",
    password: "",
    error: "",
  });

  const [loading, setLoading] = useState(false);

  const handleSignin = async (email, password) => {
    setLoading(true);

    const response = await fetch("./api/signin", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ email, password }),
    });

    if (response.status !== 200) {
      setLoading(false);
      throw new Error(await response.text());
    }

    setLoading(false);
    Router.push("/feed");
  };

  async function handleSubmit(event) {
    event.preventDefault();
    setUserData({ ...userData, error: "" });

    const email = userData.email;
    const password = userData.password;

    try {
      await handleSignin(email, password);
    } catch (error) {
      console.error(error);
      setUserData({ ...userData, error: error.message });
      setLoading(false);
    }
  }

  return (
    <div className="container">
      <div className="row justify-content-center mt-5">
        <div className="col-4 border p-4 rounded">
          <h2 className="text-center">Welcome Back</h2>
          <form onSubmit={handleSubmit}>
            <div className="form-group">
              <label htmlFor="email">Email</label>

              <input
                type="text"
                id="email"
                name="email"
                className="form-control"
                value={userData.email}
                onChange={(event) =>
                  setUserData(
                    Object.assign({}, userData, { email: event.target.value })
                  )
                }
              />
            </div>

            <div className="form-group">
              <label htmlFor="password">Password</label>
              <input
                type="password"
                id="password"
                name="password"
                className="form-control"
                value={userData.password}
                onChange={(event) =>
                  setUserData(
                    Object.assign({}, userData, {
                      password: event.target.value,
                    })
                  )
                }
              />
            </div>

            <button
              type="submit"
              disabled={loading}
              className="btn btn-primary w-100 mt-3"
            >
              {loading ? "Loading..." : "Signin"}
            </button>

            {userData.error && (
              <small className="mt-3 text-danger">
                Error: {userData.error}
              </small>
            )}
          </form>
        </div>
      </div>
    </div>
  );
}

export default signin;
```

Basically what is going on here is onSubmit of the form, the handleSubmit function is invoked,  this also invokes the handle signup async function that makes the actual API call to `api/sign`in file using fetch, the API then takes it up from there

### Signin API

Now create a new file in `/pages/api/signin.js` and duplicate this code:

```jsx
import { query } from "faunadb";
const { Match, Index, Login } = query;
import { serverClient, serializeFaunaCookie } from "../../utils/fauna-auth";

export default async function signin(req, res) {
  const { email, password } = await req.body;

  try {
    if (!email || !password) {
      throw new Error("Email and password are required.");
    }

    return serverClient
      .query(
        Login(Match(Index("users_by_email"), email), {
          password,
        })
      )
      .then((loginRes) => {
        if (!loginRes.secret) {
          throw new Error("No secret present in login query response.");
        }

        const cookieSerialized = serializeFaunaCookie(loginRes.secret);

        res.setHeader("Set-Cookie", cookieSerialized);
        res.status(200).end();
      })
      .catch((err) => {
        console.log({ err });
        // new Error("something went wrong");
        res.status(400).send(err.message);
      });
  } catch (error) {
    res.status(400).send(error.message);
  }
}
```

This is the part where your code interacts connects the database. Match, Index and Login are some of the methods Fauna javascript driver provides,  serverClient is declared in the fauna-auth file, it creates a new instance of the faunadb client. The Match function finds the user provided to Match in the users_by_email index, the Login function then creates an authentication token for the identity returned from the match function. Simply authenticating the user.

## User Signup

The user creates an account here

### Signup page

Create a new file in `/pages/api/signup.js`

```jsx
import { useState } from "react";
import Router from "next/router";

function signup() {
  const [userData, setUserData] = useState({
    email: "",
    password: "",
    error: "",
  });

  const [loading, setLoading] = useState(false);

  const handleSignup = async (email, password) => {
    setLoading(true);

    const response = await fetch("./api/signup", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ email, password }),
    });

    if (response.status !== 200) {
      setLoading(false);
      throw new Error(await response.text());
    }

    setLoading(false);
    Router.push("/");
  };

  async function handleSubmit(event) {
    event.preventDefault();
    setUserData({ ...userData, error: "" });

    const email = userData.email;
    const password = userData.password;

    try {
      await handleSignup(email, password);
    } catch (error) {
      console.error(error);
      setUserData({ ...userData, error: error.message });
      setLoading(false);
    }
  }

  return (
    <div className="container">
      <div className="row justify-content-center mt-5">
        <div className="col-4 border p-4 rounded">
          <h2 className="text-center">Get Started</h2>
          <form onSubmit={handleSubmit}>
            <div className="form-group">
              <label htmlFor="email">Email</label>

              <input
                type="text"
                id="email"
                name="email"
                className="form-control"
                value={userData.email}
                onChange={(event) =>
                  setUserData(
                    Object.assign({}, userData, { email: event.target.value })
                  )
                }
              />
            </div>

            <div className="form-group">
              <label htmlFor="password">Password</label>
              <input
                type="password"
                id="password"
                name="password"
                className="form-control"
                value={userData.password}
                onChange={(event) =>
                  setUserData(
                    Object.assign({}, userData, {
                      password: event.target.value,
                    })
                  )
                }
              />
            </div>

            <button
              type="submit"
              disabled={loading}
              className="btn btn-primary w-100 mt-3"
            >
              {loading ? "Loading..." : "Signup"}
            </button>

            {userData.error && (
              <small className="mt-3 text-danger">
                Error: {userData.error}
              </small>
            )}
          </form>
        </div>
      </div>
    </div>
  );
}

export default signup;
```

The signup page follows the same approach as the signin page but the endpoint to fetch is `/pages/api/signup` let's create the API for that request next.

### Signup API

Now create a new file in `/pages/api/signup.js` and duplicate this code:

```jsx
import { query } from "faunadb";
const { Function: Fn, Call } = query;
import { serverClient, serializeFaunaCookie } from "../../utils/fauna-auth";

export default async function signup(req, res) {
  const { email, password } = await req.body;

  try {
    if (!email || !password) {
      throw new Error("Email and password are required.");
    }

    return serverClient
      .query(Call(Fn("signupUser"), [email, password]))
      .then((loginRes) => {
        if (!loginRes.secret) {
          throw new Error("No secret present in login query response.");
        }

        const cookieSerialized = serializeFaunaCookie(loginRes.secret);

        res.setHeader("Set-Cookie", cookieSerialized);
        res.status(200).end();
      })
      .catch((err) => {
        console.log({ err });
        res.status(400).send(err.message);
      });
  } catch (error) {
    res.status(400).send(error.message);
  }
}
```

The looks similar to the signin API but there's some little difference, here we are making use of a User Defined Function `signupUser` to extract the logic and use the Call function to execute the function directly in a query.

## Accounts

User browses through a list of other user accounts on the database, the user can then choose to follow or unfollow accounts, this process creates the social graph that would be used to query feed for the user through their relationship.

### Accounts page

Create the accounts page `/pages/accounts.js`

```jsx
import { useEffect } from "react";
import { useRouter } from "next/router";
import useSWR, { mutate } from "swr";
import { withAuthSync } from "../utils/auth";

const fetcher = (url) =>
  fetch(url).then((res) => {
    if (res.status >= 300) {
      throw new Error("API Client error");
    }

    return res.json();
  });

const followAccount = async (followee) => {
  const response = await fetch("./api/follow-account", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ followee }),
  });

  if (response.status !== 200) {
    throw new Error(await response.text());
  }
  mutate("/api/list-accounts");
};

const Accounts = () => {
  const router = useRouter();
  const { data: accounts, error } = useSWR("./api/list-accounts", fetcher);
  useEffect(() => {
    if (error) router.push("/");
  }, [error, router]);
  console.log(accounts);
  return (
    <div>
      <h1>This page shows a list of accounts and the option to follow them</h1>
      {error ? (
        <h1>An error has occurred: {error.message}</h1>
      ) : accounts ? (
        accounts.map((user, index) => (
          <div key={index}>
            <hr />
            <p>
              {user.isSelf ? "Your" : "This"} user id is <b>{user.userId}</b>{" "}
            </p>
            <p>
              Currently, you are{" "}
              {user.isFollowee ? "following" : "not following"}{" "}
              {user.isSelf ? "yourself" : "this user"}{" "}
              <button onClick={() => followAccount(user.userId)}>
                {user.isFollowee ? "Unfollow" : "Follow"}
              </button>
            </p>
          </div>
        ))
      ) : (
        <h1>Loading...</h1>
      )}
      <style jsx>{`
        h1 {
          margin-bottom: 0;
        }
      `}</style>
    </div>
  );
};

export default withAuthSync(Accounts);
```

### List accounts API

create the list accounts API at `/api/list-accounts`

```jsx
import { query } from "faunadb";
import { faunaClient, FAUNA_SECRET_COOKIE } from "../../utils/fauna-auth";
import cookie from "cookie";
const { Call, Function: Fn } = query;

export default async function listAccounts(req, res) {
  //read the cookie

  const cookies = cookie.parse(req.headers.cookie ?? "");
  const faunaSecret = cookies[FAUNA_SECRET_COOKIE];
  //when there's no cookie, send status 401, Unauthorized
  if (!faunaSecret) {
    return res.status(401).send("Auth cookie missing.");
  }
  let after = await req.query.after;

  try {
    // use query to call Fauna function
    const usersPage = await faunaClient(faunaSecret)
      .query(Call(Fn("listAccounts"), after || [null]))
      .catch((error) => {
        console.log(error);
        return error;
      });
    //catch errors in case there's any
    if (usersPage.message) {
      //if the error has the message 'unauthorized', send status 401 as the login is invalid
      if (usersPage.message == "unauthorized")
        return res.status(401).send("invalid Auth cookie");
      throw new Error(usersPage.message);
    }

    res.status(200).send(usersPage.data);
  } catch (err) {
    console.log(err);
    return res.status(400).send(err.message);
  }
}
```

## Feed

After the user is successfully authenticated, the user can query a feed of posts from their relationships, i.e the follower and followed. The user also makes new posts from this page. So the feed page would be interacting with two APIs one is list-feed and the other is the create-post.

### Feed page

First create the feed page `/pages/feed.js`

```jsx
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import useSWR, { mutate } from "swr";
import { withAuthSync } from "../utils/auth";

const fetcher = (url) =>
  fetch(url).then((res) => {
    if (res.status >= 300) {
      throw new Error("API Client error");
    }

    return res.json();
  });

const Feed = () => {
  const router = useRouter();
  const { data: feed, error: feedErr } = useSWR("./api/list-feed", fetcher);
  useEffect(() => {
    if (feedErr) console.log(feedErr);
  }, [feedErr, router]);
  console.log(feed);

  const [postData, setPostData] = useState({
    description: "",
    error: "",
  });
  const [posting, setPosting] = useState(false);

  const createNewPost = async (content) => {
    setPosting(true);

    const response = await fetch("./api/create-post", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ description: content.target.description.value }),
    });

    if (response.status !== 200) {
      setPosting(false);
      throw new Error(await response.text());
    }

    setPosting(false);
    setPostData({ ...postData, description: "" });
  };

  const handleSubmit = async (content) => {
    content.preventDefault();

    try {
      await createNewPost(content);
    } catch (error) {
      console.error(error);
      setPostData({ ...postData, error: error.message });
    }
  };

  return (
    <div>
      <div className="container">
        <div className="row justify-content-center mt-5">
          <div className="col-4 border p-4 rounded">
            <h2 className="text-center mb-4">Hi, What's Popping</h2>
            <form onSubmit={handleSubmit}>
              <div className="form-group">
                <textarea
                  rows="4"
                  disabled={posting}
                  type="text"
                  name="description"
                  className="form-control"
                  value={postData.description}
                  onChange={(event) =>
                    setPostData(
                      Object.assign({}, postData, {
                        description: event.target.value,
                      })
                    )
                  }
                />
              </div>

              <button
                type="submit"
                disabled={posting}
                className="btn btn-primary w-100 mt-3"
              >
                {posting ? "Posting..." : "Post"}
              </button>

              {postData.error && (
                <small className="mt-3 text-danger">
                  Error: {postData.error}
                </small>
              )}
            </form>
          </div>
        </div>
      </div>
    </div>
  );
};

export default withAuthSync(Feed);
```

Using the useEffect hook, we fetch the feed data for the user immediately after the component has been mounted. After the user enters their post in the textarea and submits it, the create-post API is invoked, this saves the post data in fauna, since we're using stale while revalidate (SWR) we can immediately have the new post data appear on the page without refreshing by calling the mutate method from the SWR library after successfully creating the post, this revalidates the post data ensuring it's up to date.

### List Feed API

create the list feed API at `/api/list-feed`

```jsx
import { query } from "faunadb";
import { faunaClient, FAUNA_SECRET_COOKIE } from "../../utils/fauna-auth";
import cookie from "cookie";
const { Call, Function: Fn } = query;

export default async function listAccounts(req, res) {
  //read the cookie

  const cookies = cookie.parse(req.headers.cookie ?? "");
  const faunaSecret = cookies[FAUNA_SECRET_COOKIE];
  //when there's no cookie, send status 401, Unauthorized
  if (!faunaSecret) {
    return res.status(401).send("Auth cookie missing.");
  }
  let after = await req.query.after;

  try {
    // use query to call Fauna function
    const usersPage = await faunaClient(faunaSecret)
      .query(Call(Fn("getFeed"), after || [null]))
      .catch((error) => {
        console.log(error);
        return error;
      });
    //catch errors in case there's any
    if (usersPage.message) {
      //if the error has the message 'unauthorized', send status 401 as the login is invalid
      if (usersPage.message == "unauthorized")
        return res.status(401).send("invalid Auth cookie");
      throw new Error(usersPage.message);
    }

    res.status(200).send(usersPage.data);
  } catch (err) {
    console.log(err);
    return res.status(400).send(err.message);
  }
}
```

This API returns the post feed for the user, it also utilizes the User Defined Function by using the Call function to execute the getFeed function in the query.

### Create Post API

then create post API at `/pages/api/create-post`

```jsx
import { query } from "faunadb";
import { faunaClient, FAUNA_SECRET_COOKIE } from "../../utils/fauna-auth";
import cookie from "cookie";
const { Call, Function: Fn } = query;

export default async function createPost(req, res) {
  const cookies = cookie.parse(req.headers.cookie ?? "");
  const faunaSecret = cookies[FAUNA_SECRET_COOKIE];

  if (!faunaSecret) {
    return res.status(401).send("Auth cookie missing.");
  }

  return faunaClient(faunaSecret)
    .query(Call(Fn("createPost"), req.body.description))
    .then((newPostContent) => {
      res.status(200).send(newPostContent);
    })
    .catch((err) => {
      console.log(err);
      if (err.message) {
        if (err.message == "unauthorized")
          return res.status(401).send("invalid Auth cookie");
        throw new Error(err.message);
      }
      return res.status(400).send(err.message);
    });
}
```

This creates the new post and saves it in Fauna, we have completed the scope for this tutorial. Congratulations if you made it this far

# Conclusion

In this tutorial, we have been able to model out a small social graph that has some of the features of a social application, although this is not close to the complexity of a real-life social graph. There's still more to dig into such as the Attribute-Based Access Control (ABAC), like and dislike a post, comment on a post and many more. Fauna is the kind of database that factors in developer's happiness out of the box