Building a Product Hunt clone app using Hasura and Next.js
Tags: nextjs, hasura, tutorial, webdev
Published: 2020-02-24 16:02:42

---
title: Building a Product Hunt clone app using Hasura and Next.js
published: true
date: 2020-02-20 19:00:38 UTC
tags: nextjs,hasura,tutorial,webdev
canonical_url: https://blog.logrocket.com/building-a-product-hunt-clone-app-using-hasura-and-next-js/
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/6xnh725qisiaw7i5z3ic.png
---
**Written by [Nirmalya Ghosh](https://blog.logrocket.com/author/nirmalyaghosh/)**✏️

This tutorial will demonstrate how to create a clone of product curation app [Product Hunt](https://www.producthunt.com/) using [Hasura](https://hasura.io/) and [Next.js](https://nextjs.org/). We’ll show you how to conduct authentication and authorization using Hasura and how to leverage GraphQL to fetch and write data from a Next.js frontend application.

We’ll discuss these tasks within the context of three applications:

1. Frontend using Next.js
2. Backend using Hasura (which uses the Postgres database)
3. Authentication using Node.js

The whole project is available on [GitHub](https://github.com/ghoshnirmalya/product-hunt-clone-using-nextjs-and-hasura).

## What are Next.js and Hasura?

I’ve been using [Next.js](https://dev.to/bnevilleoneill/what-s-new-in-next-js-9-2-4d3k) for over a year and have been satisfied with the overall developer experience because it supports many exciting features, such as [server-side rendering](https://nextjs.org/features/server-side-rendering), out of the box.

Hasura is an engine that autogenerates GraphQL APIs, as well as migration files for creating, updating, and deleting tables and columns in the Postgres database.

[![LogRocket Free Trial Banner](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2017/03/f760c-1gpjapknnuyhu8esa3z0jga.png?resize=1200%2C280&ssl=1)](https://logrocket.com/signup/)

## Getting started with Hasura

Let’s start by bootstrapping our backend application.

First, we’ll need to install the [Hasura CLI](https://docs.hasura.io/1.0/graphql/manual/hasura-cli/index.html#installation). Once it’s installed, we can initialize the directory for storing Hasura GraphQL migrations.

```jsx
hasura init --directory backend --endpoint http://localhost:8080 --admin-secret secret
```

For more details about the flags `--directory`, `--endpoint`, and `--admin-secret`, see the official [Hasura documentation](https://docs.hasura.io/1.0/graphql/manual/hasura-cli/hasura_init.html#options).

![Initializing a Hasura Application Using Hasura CLI](https://i1.wp.com/blog.logrocket.com/wp-content/uploads/2020/02/initializing-hasura.png?resize=730%2C223&ssl=1)

The above command will create a backend directory.

![Initializing the Backend Application](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2020/02/initializing-backend-app.png?resize=730%2C288&ssl=1)

The `config.yam` file will contain the following.

```jsx
// backend/config.yaml

admin_secret: secret
endpoint: http://localhost:8080
```

We’ll use [Docker](https://dev.to/bnevilleoneill/docker-for-front-end-developers-ldi) to install Hasura. According to the [official docs](https://docs.docker.com/install/), it supports Mac, Windows, and Linux. We’ll need to create a new file called `Dockerfile` inside our backend directory.

```jsx
// backend/Dockerfile

FROM hasura/graphql-engine:v1.0.0
RUN mkdir /backend-app
WORKDIR /backend-app
COPY . /backend-app/
```

Let’s also create a `.env` file to store all the credentials for Hasura.

```jsx
// backend/.env

HASURA_GRAPHQL_DATABASE_URL=postgres://postgres:@db:5432/postgres
HASURA_GRAPHQL_ENABLE_CONSOLE=false
HASURA_GRAPHQL_MIGRATIONS_DIR=./migrations
ENDPOINT=http://localhost:8080
HASURA_GRAPHQL_ADMIN_SECRET=secret
HASURA_GRAPHQL_JWT_SECRET={"type": "RS256", "key": ""}
```

It’s always a good idea to [store credentials in environment files](https://medium.com/poka-techblog/the-best-way-to-store-secrets-in-your-app-is-not-to-store-secrets-in-your-app-308a6807d3ed). We’ll add the `HASURA_GRAPHQL_JWT_SECRET` after we initialize our authentication application.

That’s all the coding we need to do for our backend application. The migration files will be generated automatically when we add tables and columns to our database.

## Getting started with Next.js

We’ll create the Next.js application inside a new `frontend` directory. We’ll use Docker for Next.js as well.

Next.js has many [examples](https://github.com/zeit/next.js/tree/canary/examples/with-docker) to bootstrap our application. We’ll use the `with-docker` example.

```jsx
yarn create next-app --example with-docker with-docker-app
```

![Initializing a Next.js Application Using next-app](https://i1.wp.com/blog.logrocket.com/wp-content/uploads/2020/02/initilializing-nextjs-app-next-app.png?resize=730%2C897&ssl=1)

The above command will generate a `frontend` directory.

![Initializing the Frontend Application](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2020/02/initializing-frontend-app.png?resize=730%2C385&ssl=1)

Let’s update the contents of `Dockerfile` with the follwing.

```jsx
// frontend/Dockerfile

FROM mhart/alpine-node:11.1.0
RUN mkdir /frontend-app
WORKDIR /frontend-app
RUN apk update && apk upgrade && apk add --no-cache bash git
COPY . /frontend-app/
RUN yarn install
EXPOSE 3000
CMD ["yarn", "dev"]
```

## Building an authentication app

We’ll set up our authentication application using [JWT](https://blog.logrocket.com/jwt-authentication-best-practices/), Hasura claims, and multiple roles. We’ll use [this repository](https://github.com/hasura/graphql-engine/tree/master/community/boilerplates/auth-servers/passportjs-jwt-roles) to initialize our application according to the following steps.

**Step 1.** Create a new directory called `authentication`

```jsx
mkdir authentication
```

**Step 2.** Clone the repository

```jsx
git clone https://github.com/hasura/graphql-engine
```

**Step 3.** Visit the `passportjs-jwt-roles` directory

```jsx
cd graphql-engine/community/boilerplates/auth-servers/passportjs-jwt-roles
```

**Step 4.** Move this directory to the `authentication` directory we just created

```jsx
mv * ../../../../../authentication
```

**Step 5.** Create a `.env` file inside the authentication directory with the following content

```jsx
//authentication/.envDATABASE_URL=postgres://postgres:@db:5432/postgres
```

**Step 6.** Install the dependencies

```jsx
yarn install
```

**Step 7.** Generate the RSA keys

```jsx
openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -pubout > public.pem
```

**Step 8.** Print the keys in an escaped format

```jsx
awk -v ORS='\n' '1' public.pem 
```

**Step 9.** Add the output into the `HASURA_GRAPHQL_JWT_SECRET` key present in the `.env` file of our backend application.

```jsx
// backend/.env

...
HASURA_GRAPHQL_JWT_SECRET={"type": "RS256", "key": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvdJnVbZrrfYGGrnZTQwq\nlOZGonXh5h6yDqN7Np1KRmz8EyZKulKn186StNt2TZJPyYdAcAH6vnB05LUS47VF\nv9eIaAn8952BnJe0NZ9o+90k03wADORNzzeNqmJWGn2YbBiJHNLDt21Zn9dbOWOE\nKjO7eWOuiKEYEyC/OW9+p+Z05ahCZvmWVzNu9ylnHb8wqlFpwaGIEBn4Cm5/C7Zs\nj6BgggZOKp0GN6XNsc8+4zU1h2CpcCjyDJcNN1mGTMSLQHNOLAI4FY/qLwJcSomf\n8d7vTn+8EsJl+Ur+73qIYKpGoGiJw6Y2qpviRCttR8QF21QlIynz71YZTMl1SBmV\nkwIDAQAB\n-----END PUBLIC KEY-----\n"}
```

Our authentication application should have the following structure.

![Initializing Our Authentication Application](https://i1.wp.com/blog.logrocket.com/wp-content/uploads/2020/02/initializing-authentication-app.png?resize=730%2C598&ssl=1)

## Restructuring the application tree

If we view our application structure from the root, we’ll see the following.

![Structure of Our Applications](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2020/02/structure-of-apps-product-hunt-clone.png?resize=730%2C308&ssl=1)

Let’s remove the `graphql-engine` directory from the root since we won’t need that anymore.

```jsx
rm -rf graphql-engine
```

Next, we’ll create a new `docker-compose.yml` file, which will be responsible for composing our applications with the following content.

```jsx
// docker-compose.yml

version: "3"
services:
  backend:
    build:
      context: ./backend
    depends_on:
      - db
    env_file: ./backend/.env
    ports:
      - 8080:8080
    volumes:
      - ./backend:/backend-app
  frontend:
    build:
      context: ./frontend
    command: yarn dev
    depends_on:
      - backend
      - authentication
    volumes:
      - ./frontend:/frontend-app
      - ./frontend/package.json:/frontend-app/package.json
      - ./frontend/yarn.lock:/frontend-app/yarn.lock
    ports:
      - "3000:3000"
  authentication:
    build:
      context: ./authentication
    command: yarn start
    env_file: ./authentication/.env
    depends_on:
      - db
    ports:
      - "3030:3030"
    volumes:
      - ./authentication:/authentication-app
      - ./authentication/package.json:/authentication-app/package.json
      - ./authentication/yarn.lock:/authentication-app/yarn.lock
  db:
    image: postgres:11.3-alpine
    volumes:
      - db_data:/var/lib/postgresql/data
volumes:
  db_data:
```

Our application tree should look like this:

![Tree Structure of Our Applications](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2020/02/tree-structure-off-apps.png?resize=730%2C308&ssl=1)

## Starting our applications using Docker

Once Docker is installed and running on our system, we can run the following command to start our applications.

```jsx
docker-compose up
```

See the [Docker documentation](https://docs.docker.com/compose/reference/up/) for more information about the above command.

We can check whether all the containers are up and running using the [`docker-compose ps`](https://docs.docker.com/compose/reference/ps/) command.

![View Running Docker Containers](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2020/02/running-docker-containers.png?resize=730%2C253&ssl=1)

Note that the name of the containers can be different.

## Running migrations to create user data in our database

In our authentication application, three migrations are present inside the migrations directory:

![Migrations for Our Authentication Application](https://i1.wp.com/blog.logrocket.com/wp-content/uploads/2020/02/migrations-for-authentication-app.png?resize=730%2C308&ssl=1)

To run these migrations, we will have to [SSH into our authentication container](https://phase2.github.io/devtools/common-tasks/ssh-into-a-container/). We can find out the names of all our containers by running the following command.

```jsx
docker-compose ps
```

![View Running Docker Containers](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2020/02/running-docker-containers-2.png?resize=730%2C253&ssl=1)

To SSH into the authentication container, we need to run the following command.

```jsx
docker exec -it authentication_1 /bin/bash
```

Once inside the container, we can run the migrations using the following command.

```jsx
bash-4.4# knex migrate:latest
```

It’ll run all the migrations from the authentication application.

![Running Migrations for Our Authentication Application](https://i1.wp.com/blog.logrocket.com/wp-content/uploads/2020/02/migrations-for-authentication-app-2.png?resize=730%2C288&ssl=1)

Now let’s look at how the above three migrations modified our database. After we exit from the container (using the command `exit`), we can go to our `backend` directory where we’ll run the following command to run the [Hasura console](https://docs.hasura.io/1.0/graphql/manual/hasura-cli/hasura_console.html).

```jsx
hasura console
```

This will open the Hasura console on [http://localhost:9695/](http://localhost:9695/). If we go to the Data tab, we’ll be able to see five untracked tables.

![Viewing Our Hasura Schema](https://i1.wp.com/blog.logrocket.com/wp-content/uploads/2020/02/hasura-schema.png?resize=730%2C410&ssl=1)

We need to click the **Track** button next to `role`, `user`, and `user_role`. These weren’t tracked because they weren’t exposed through the GraphQL API.

Next, we’ll click the **Track All** button next to **Untracked foreign-key relations** to track all relationships. We can view the relationships of each table from the **Relationships** tab inside each table.

![Tracking Foreign-Key Relations](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2020/02/foreign-key-relations-hasura.png?resize=730%2C411&ssl=1)

![Relationships of the user_role Table](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2020/02/user_role-tables-hasura.png?resize=730%2C411&ssl=1)

We can now view all the tables our migrations created.

The `role` table has two columns:

1. `id` (uuid, primary key, unique; default: `gen_random_uuid()`)
2. `name` — (character varying, unique)

The `user` table has five columns:

1. `id` — (uuid, primary key, unique; default: `gen_random_uuid()`)
2. `username` — (character varying, unique)
3. `password` — (character varying)
4. `created_at` — (timestamp with time zone, default: `now()`)
5. `active` — (boolean, nullable, default: `true`)

The `user_role` table has three columns:

1. `id` — (uuid, primary key, unique, default: `gen_random_uuid()`)
2. `role_id` — (uuid, nullable)
3. `user_id` — (uuid, nullable)

Now if we visit the [Hasura GraphQL Explorer](http://localhost:9695/api-explorer), we’ll be able to query our database using GraphiQL.

![Checking GraphQL Data Using GraphiQL](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2020/02/graphql-data-graphiql.png?resize=730%2C409&ssl=1)

Since we don’t have any user data, the response is an empty `user` array. It’s interesting to note that if we don’t send the `x-hasura-admin-secret` key (whose value is `secret`) with the request header, we won’t be able to query any data.

![Sending x-hasura-admin-secret in the Request Header Is Mandatory](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2020/02/hasura-request-header.png?resize=730%2C407&ssl=1)

That’s because we’ve set the value of `HASURA_GRAPHQL_ADMIN_SECRET` to `secret` in the `.env` file as a part of [securing our GraphQL endpoint](https://docs.hasura.io/1.0/graphql/manual/deployment/heroku/securing-graphql-endpoint.html).

![Securing Our API Endpoints Using HASURA_GRAPHQL_ADMIN_SECRET](https://i1.wp.com/blog.logrocket.com/wp-content/uploads/2020/02/securing-api-endpoints-hasura.png?resize=730%2C230&ssl=1)

Also, we need to remember that any changes we make through the Hasura console will create migrations inside our `migration` directory, which is present in the backend application’s directory.

![Hasura Console Automatically Generates Migration Files](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2020/02/auto-generate-migration-files-hasura.png?resize=730%2C288&ssl=1)

## Authenticating users with our authentication app

If we follow our authentication repository’s [documentation](https://github.com/hasura/graphql-engine/tree/master/community/boilerplates/auth-servers/passportjs-jwt-roles#usage), we can initiate the following CURL request to create a new user.

```jsx
curl -H "Content-Type: application/json" \
     -d'{"username": "admin1", "password": "pass", "confirmPassword": "pass"}'\
     http://localhost:3030/signup
```

We’ll get the following response.

![Response for Creating a New User in Hasura](https://i1.wp.com/blog.logrocket.com/wp-content/uploads/2020/02/new-user-response-hasura.png?resize=730%2C373&ssl=1)

We can also do a similar CURL request to log in an existing user.

```jsx
curl -H "Content-Type: application/json" \
     -d'{"username": "admin1", "password": "pass"}' \
     http://localhost:3030/login
```

We’ll get the following response.

![Response for Logging In an Existing User in Hasura](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2020/02/existing-user-login-hasura.png?resize=730%2C388&ssl=1)

## Integrating the authentication application with our frontend application

Let’s connect our Next.js application with our authentication application. The frontend application is running on [http://localhost:3000/](http://localhost:3000/).

We’ll be using [Chakra UI](https://chakra-ui.com/getting-started) to build our frontend application and [Nookies](https://github.com/maticzav/nookies) to handle cookies. To install them, we need to SSH into our frontend container.

```jsx
docker exec -it frontend_1 /bin/bash
```

Once we’re inside the container, we can install the package using the following command.

```jsx
bash-4.4# yarn add @chakra-ui/core @emotion/core @emotion/styled emotion-theming nookies
```

We’ll need to create a new file named `_app.js` inside the `pages` directory with the following content.

```jsx
// frontend/pages/_app.js

import React from 'react'
import NextApp from 'next/app'
import { ThemeProvider, CSSReset, theme } from '@chakra-ui/core'

class App extends NextApp {
  render() {
    const { Component } = this.props
    return (
      <ThemeProvider theme={theme}>
        <CSSReset />
        <Component />
      </ThemeProvider>
    )
  }
}

export default App
```

Let’s add the the `Button` component in Chakra UI to our index page.

```jsx
// frontend/pages/index.js

import React from 'react'
import { Box, Button } from "@chakra-ui/core";

const IndexPage = () => {
  return (
    <Box
      w="100%"
      minH="100vh"
      p={4}
      d="flex"
      alignItems="center"
      justifyContent="center"
    >
      <Button>Hello World!</Button>
    </Box>
  )
}

export default IndexPage
```

![Landing Page of Our Frontend Application](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2020/02/frontend-app-loading-page.png?resize=730%2C270&ssl=1)

Now let’s create the sign-up form. We’ll have three fields: Username, Password and Confirm Password.

```jsx
// frontend/components/forms/sign-up.js

import React, { useState } from "react";
import {
  Alert,
  AlertIcon,
  Box,
  Button,
  FormControl,
  FormLabel,
  FormHelperText,
  Input
} from "@chakra-ui/core";
import { setCookie } from "nookies";

const SignUpForm = () => {
  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  const [confirmPassword, setConfirmPassword] = useState("");
  const [error, setError] = useState(null);

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

  const response = await fetch("http://localhost:3030/signup", {
      method: "POST",
      body: JSON.stringify({
        username,
        password,
        confirmPassword
      }),
      headers: {
        "Content-type": "application/json; charset=UTF-8"
      }
    });

    const data = await response.json();

    if (response.status !== 200) {
      setError("Something went wrong! Please try again.");
    } else {
      setCookie({}, "X-Hasura-User-Id", data.id, {
        maxAge: 30 * 24 * 60 * 60,
        path: "/"
      });
      setCookie({}, "token", data.token, {
        maxAge: 30 * 24 * 60 * 60,
        path: "/"
      });
    }
  };

  const errorsNode = () => {
    if (!error) return false;
    return (
      <Alert status="error" mb={8} rounded="md" variant="left-accent">
        <AlertIcon />
        {error}
      </Alert>
    );
  };

  return (
    <Box
      w="100%"
      minH="100vh"
      p={4}
      d="flex"
      alignItems="center"
      justifyContent="center"
    >
      <Box w="500px" p={8} bg="gray.50" rounded="md">
        {errorsNode()}
        <FormControl mb={8}>
          <FormLabel htmlFor="username">Username</FormLabel>
          <Input
            type="text"
            id="username"
            aria-describedby="john"
            value={username}
            onChange={e => setUsername(e.target.value)}
          />
        </FormControl>
        <FormControl mb={8}>
          <FormLabel htmlFor="password">Password</FormLabel>
          <Input
            type="password"
            id="password"
            aria-describedby="*****"
            value={password}
            onChange={e => setPassword(e.target.value)}
          />
        </FormControl>
        <FormControl mb={8}>
          <FormLabel htmlFor="confirm-password">Confirm Password</FormLabel>
          <Input
            type="password"
            id="confirm-password"
            aria-describedby="*****"
            value={confirmPassword}
            onChange={e => setConfirmPassword(e.target.value)}
          />
          {!!confirmPassword && confirmPassword !== password && (
            <FormHelperText id="confirm-password">
              Passwords do not match
            </FormHelperText>
          )}
        </FormControl>
        <FormControl>
          <Button bg="#764ABC" color="white" onClick={handleSubmit}>
            Sign Up
          </Button>
        </FormControl>
      </Box>
    </Box>
  );
};

export default SignUpForm;
```

We need to import the form in our index page.

```jsx
// frontend/pages/index.js

import React, { Component } from "react";
import SignUpForm from "../components/forms/sign-up";

export default class IndexPage extends Component {
  render() {
    return <SignUpForm />;
  }
}
```

The above code will generate the following form on [http://localhost:3000/](http://localhost:3000/).

![Sign-Up Form Using Chakra UI](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2020/02/sign-up-form-chakra-ui.png?resize=730%2C411&ssl=1)

Let’s take a closer look at the `handleSubmit` function of our `SignUpForm` component.

First, we’ll initiate a `POST` request to [http://localhost:3030/signup](http://localhost:3030/signup), which is our sign-up API endpoint. If we get a `success` response from our API, that means we’re storing the `id` and `token` of the user from the response in cookies. If we check our browser cookies, we’ll see that two cookies are now set in our browser.

![Setting Cookies for the User From the Response](https://i1.wp.com/blog.logrocket.com/wp-content/uploads/2020/02/setting-up-cookies-hasura.png?resize=730%2C411&ssl=1)

The `X-Hasura-User-Id` cookie will be necessary to [authorize users](https://docs.hasura.io/1.0/graphql/manual/auth/authorization/basics.html) in Hasura. We can also store the type of user, but that’s not necessary in our application. We’ll use this cookie while building authorization in the backend application.

Let’s create a similar sign-in form with only two fields: Username and Password **.**

```jsx
// frontend/components/forms/sign-in.js

import React, { useState } from "react";
import {
  Alert,
  AlertIcon,
  Box,
  Button,
  FormControl,
  FormLabel,
  Input
} from "@chakra-ui/core";
import { setCookie } from "nookies";

const SignInForm = () => {
  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  const [error, setError] = useState(null);

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

    const response = await fetch("http://localhost:3030/login", {
      method: "POST",
      body: JSON.stringify({
        username,
        password
      }),
      headers: {
        "Content-type": "application/json; charset=UTF-8"
      }
    });

    const data = await response.json();

    if (response.status !== 200) {
      setError("Something went wrong! Please try again.");
    } else {
      setCookie({}, "X-Hasura-User-Id", data.id, {
        maxAge: 30 * 24 * 60 * 60,
        path: "/"
      });
      setCookie({}, "token", data.token, {
        maxAge: 30 * 24 * 60 * 60,
        path: "/"
      });
    }
  };

  const errorsNode = () => {
    if (!error) return false;
    return (
      <Alert status="error" mb={8} rounded="md" variant="left-accent">
        <AlertIcon />
        {error}
      </Alert>
    );
  };

  return (
    <Box
      w="100%"
      minH="100vh"
      p={4}
      d="flex"
      alignItems="center"
      justifyContent="center"
    >
      <Box w="500px" p={8} bg="gray.50" rounded="md">
        {errorsNode()}
        <FormControl mb={8}>
          <FormLabel htmlFor="username">Username</FormLabel>
          <Input
            type="text"
            id="username"
            aria-describedby="john"
            value={username}
            onChange={e => setUsername(e.target.value)}
          />
        </FormControl>
        <FormControl mb={8}>
          <FormLabel htmlFor="password">Password</FormLabel>
          <Input
            type="password"
            id="password"
            aria-describedby="*****"
            value={password}
            onChange={e => setPassword(e.target.value)}
          />
        </FormControl>
        <FormControl>
          <Button bg="#764ABC" color="white" onClick={handleSubmit}>
            Sign In
          </Button>
        </FormControl>
      </Box>
    </Box>
  );
};

export default SignInForm;
```

Let’s import that form in our `login` page.

```jsx
// frontend/pages/login.js

import React, { Component } from "react";
import SignInForm from "../components/forms/sign-in";

export default class SignInPage extends Component {
  render() {
    return <SignInForm />;
  }
}
```

Now if we visit [http://localhost:3000/login](http://localhost:3000/login), we should be able to see our login form.

![SignInForm Component](https://i1.wp.com/blog.logrocket.com/wp-content/uploads/2020/02/sign-in-form-component.png?resize=730%2C411&ssl=1)

Now, let’s create another component where we’ll show a list of products. Currently, we don’t have any products. So let’s create some.

```jsx
// frontend/components/lists/products.js

import React from "react";
import { Grid, Box } from "@chakra-ui/core";

const ProductsList = () => {
  return (
    <Box
      w="100%"
      minH="100vh"
      p={4}
      d="flex"
      alignItems="center"
      justifyContent="center"
    >
      <Box w="800px" p={8} bg="gray.50" borderRadius="sm">
        <Grid
          templateColumns={[
            "repeat(1, 1fr)",
            "repeat(1, 1fr)",
            "repeat(2, 1fr)"
          ]}
          gap={8}
        >
          <Box w="100%" h={24} bg="#764ABC" rounded="md" />
          <Box w="100%" h={24} bg="#764ABC" rounded="md" />
          <Box w="100%" h={24} bg="#764ABC" rounded="md" />
          <Box w="100%" h={24} bg="#764ABC" rounded="md" />
          <Box w="100%" h={24} bg="#764ABC" rounded="md" />
          <Box w="100%" h={24} bg="#764ABC" rounded="md" />
        </Grid>
      </Box>
    </Box>
  );
};

export default ProductsList;
```

We’ll show these components on the `products` page.

```jsx
// frontend/pages/products/index.js

import React, { Component } from "react";
import ProductsList from "../components/lists/products";

export default class ProductsPage extends Component {
  render() {
    return <ProductsList />;
  }
}
```

If we visit [http://localhost:3000/products](http://localhost:3000/products), we should now see the following screen.

![Products List Component](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2020/02/products-list-component.png?resize=730%2C411&ssl=1)

For now, we’re just showing placeholder content instead of the products themselves. The next step is to create a table to store the products in our backend application.

## Creating tables for storing our products in Hasura

Let’s build the feature to store and retrieve products from our database using Hasura.

If we change the directory to our backend application and run the `hasura console` command, the console will be up and running at [http://localhost:9695/](http://localhost:9695/).

![Hasura Console Up and Running on http://localhost:9695/](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2020/02/hasura-console.png?resize=730%2C407&ssl=1)

Let’s click on the **Data** tab and add a new table called `product`, which will have the following six columns.

1. 
  1. `id` (uuid, primary key, unique, default: `gen_random_uuid()`)
  2. `name` (character)
  3. `description` (character)
  4. `created_at` (timestamp with time zone, default: `now()`)
  5. `updated_at` (timestamp with time zone, default: `now()`)
  6. `maker_id` (uuid)

![Product Model in Hasura](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2020/02/product-model-hasura.png?resize=730%2C401&ssl=1)

We’ll also need to add a relationship between the product and the maker (the user who added the product). We’d use this relationship to authorize editing of that item.

To establish the relationship between the product and user model, we need to do the following:

1. Click on the **Relationships** tab inside the product model
2. Click the Configure button below **Add a new relationship manually**
3. Select **Object Relationship** from the **Relationship Type** dropdown
4. Type “maker” as the Relationship Name
5. Select **public** from the **Reference Schema** dropdown
6. Select **user** as the **Reference Table**
7. Select **maker\_id** from the **From** dropdown and **id** from the **To** dropdown

We’ve now defined a relationship between the product and user models.

![Relationship Between Product and User Models](https://i1.wp.com/blog.logrocket.com/wp-content/uploads/2020/02/product-user-models-hasura.png?resize=730%2C409&ssl=1)

You can find more information about Relationships in the [Hasura documentation](https://docs.hasura.io/1.0/graphql/manual/schema/relationships/index.html).

Now let’s define a new relationship between the User and Product models.

1. Click on the **Relationships** tab inside the user model
2. Click the **Configure** button below **Add a new relationship manually**
3. Select **Array Relationship** from the **Relationship Type** dropdown
4. Type “products” as the **Relationship Name**
5. Select **public** from the **Reference Schema** dropdown
6. Select **product** as the **Reference Table**
7. Select **id** from the **From** dropdown and **maker\_id** from the **To** dropdown

![Relationship Between User and Product Models in Hasura](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2020/02/user-product-models-hasura.png?resize=730%2C411&ssl=1)

Now if we visit the [Hasura GraphQL Explorer](http://localhost:9695/api-explorer), we’ll be able to query our products using GraphiQL.

![Fetching Product Data Using GraphiQL in Hasura](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2020/02/fetching-products-data-graphiql-hasura.png?resize=730%2C409&ssl=1)

## Configuring Next.js to fetch GraphQL data with GraphQL Hooks

It’s time to configure our Next.js application to fetch data from our backend application. We’ll use [GraphQL Hooks](https://github.com/nearform/graphql-hooks) to fetch data from the backend application.

To install the dependencies, we need to SSH into our frontend container.

```jsx
docker exec -it frontend_1 /bin/bash
```

Once inside the container, we can install the package using the following command.

```jsx
bash-4.4# yarn add graphql-hooks graphql-hooks-memcache graphql-hooks-ssr
```

Since we’ll be using [SSR](https://github.com/nearform/graphql-hooks#SSR), we have to create two utility files.

```jsx
// frontend/lib/with-graphql-client.js

import React from "react";
import initGraphQL from "./init-graphql";
import Head from "next/head";
import { getInitialState } from "graphql-hooks-ssr";

export default App => {
  return class GraphQLHooks extends React.Component {
    static displayName = "GraphQLHooks(App)";
    static async getInitialProps(ctx) {
      const { AppTree } = ctx;
      let appProps = {};
      if (App.getInitialProps) {
        appProps = await App.getInitialProps(ctx);
      }
      // Run all GraphQL queries in the component tree
      // and extract the resulting data
      const graphQLClient = initGraphQL();

      let graphQLState = {};

      if (typeof window === "undefined") {
        try {
          // Run all GraphQL queries
          graphQLState = await getInitialState({
            App: <AppTree {...appProps} graphQLClient={graphQLClient} />,
            client: graphQLClient
          });
        } catch (error) {
          // Prevent GraphQL hooks client errors from crashing SSR.
          // Handle them in components via the state.error prop:
          // https://github.com/nearform/graphql-hooks#usequery
          console.error("Error while running `getInitialState`", error);
        }
        // getInitialState does not call componentWillUnmount
        // head side effect therefore need to be cleared manually
        Head.rewind();
      }

      return {
        ...appProps,
        graphQLState
      };
    }

    constructor(props) {
      super(props);
      this.graphQLClient = initGraphQL(props.graphQLState);
    }

    render() {
      return <App {...this.props} graphQLClient={this.graphQLClient} />;
    }
  };
};


// frontend/lib/init-graphql.js

import { GraphQLClient } from "graphql-hooks";
import memCache from "graphql-hooks-memcache";
import unfetch from "isomorphic-unfetch";

let graphQLClient = null;

function create(initialState = {}) {
  return new GraphQLClient({
    ssrMode: typeof window === "undefined",
    url: "http://localhost:8080/v1/graphql",
    cache: memCache({ initialState }),
    fetch: typeof window !== "undefined" ? fetch.bind() : unfetch, // eslint-disable-line
    headers: {
      "x-hasura-admin-secret": "secret"
    }
  });
}

export default function initGraphQL(initialState) {
  // Make sure to create a new client for every server-side request so that data
  // isn't shared between connections (which would be bad)
  if (typeof window === "undefined") {
    return create(initialState);
  }
  // Reuse client on the client-side
  if (!graphQLClient) {
    graphQLClient = create(initialState);
  }

  return graphQLClient;
}
```

These two files are present in the `with-graphql-hooks` example of Next.js.

We now need to update our `_app.js` file because we need to wrap our frontend application with the `withGraphQLClient` [higher order function](https://medium.com/javascript-scene/higher-order-functions-composing-software-5365cf2cbe99).

```jsx
// frontend/pages/_app.js

import React from "react";
import NextApp from "next/app";
import { ThemeProvider, CSSReset, theme } from "@chakra-ui/core";
import { ClientContext } from "graphql-hooks";

import withGraphQLClient from "../lib/with-graphql-client";

class App extends NextApp {
  render() {
    const { Component, pageProps, graphQLClient } = this.props;
    return (
      <ThemeProvider theme={theme}>
        <CSSReset />
        <ClientContext.Provider value={graphQLClient}>
          <Component {...pageProps} />
        </ClientContext.Provider>
      </ThemeProvider>
    );
  }
}
export default withGraphQLClient(App);
```

## Showing product data in the Next.js app

Now let’s update our `ProductsList` component to show actual products from our database.

```jsx
// frontend/components/lists/products.js

import React from "react";
import { Text, Grid, Box, Spinner, Alert, AlertIcon } from "@chakra-ui/core";
import { useQuery } from "graphql-hooks";

export const allProductsQuery = `
  query {
    product {
      id
      name
      description
    }
  }
`;

const ProductsList = () => {
  const { loading, error, data } = useQuery(allProductsQuery, {
    skipCache: true
  });

  if (loading) {
    return (
      <Box
        w="100%"
        minH="100vh"
        d="flex"
        alignItems="center"
        justifyContent="center"
      >
        <Spinner />
      </Box>
    );
  }

  if (error) {
    return (
      <Box
        w="100%"
        minH="100vh"
        d="flex"
        alignItems="center"
        justifyContent="center"
      >
        <Alert status="error" variant="left-accent">
          <AlertIcon />
          There was an error processing your request
        </Alert>
      </Box>
    );
  }

  return (
    <Box
      w="100%"
      minH="100vh"
      p={4}
      d="flex"
      alignItems="center"
      justifyContent="center"
    >
      <Box w="800px" p={8} bg="gray.50" borderRadius="sm">
        <Grid
          templateColumns={[
            "repeat(1, 1fr)",
            "repeat(1, 1fr)",
            "repeat(2, 1fr)"
          ]}
          gap={8}
        >
          {data.product.map(item => {
            return (
              <Box
                key={item.id}
                w="100%"
                p={12}
                bg="#764ABC"
                rounded="md"
                color="white"
              >
                <Text fontSize="xl" fontWeight="semibold" lineHeight="short">
                  {item.name}
                </Text>
                <Text mt={2}>{item.description}</Text>
              </Box>
            );
          })}
        </Grid>
      </Box>
    </Box>
  );
};

export default ProductsList;
```

After making this change, if we visit [http://localhost:3000/products](http://localhost:3000/products), we should be able to see the actual products from our backend.

![List of Products From Hasura](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2020/02/list-products-hasura.png?resize=730%2C411&ssl=1)

## Extending the Next.js app to add new products

At this stage, we can build a form to add new products.

```jsx
// frontend/components/forms/add-new-product.js

import React, { useState } from "react";
import { Box, Button, FormControl, FormLabel, Input } from "@chakra-ui/core";
import { parseCookies } from "nookies";
import { useMutation } from "graphql-hooks";
import { withRouter } from "next/router";

export const insertProductMutation = `
  mutation ($name: bpchar!, $description: bpchar!, $maker_id: uuid!) {
    insert_product(objects: {description: $description, name: $name, maker_id: $maker_id}) {
      returning {
        id
        name
      }
    }
  }
`;

const AddNewProductForm = ({ router }) => {
  const [insertProduct] = useMutation(insertProductMutation);
  const [name, setName] = useState("");
  const [description, setDescription] = useState("");
  const maker_id = parseCookies()["X-Hasura-User-Id"];

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

    await insertProduct({
      variables: {
        name,
        description,
        maker_id
      }
    });
    setName("");
    setDescription("");
    router.push("/products");
  };

  return (
    <Box
      w="100%"
      minH="100vh"
      p={4}
      d="flex"
      alignItems="center"
      justifyContent="center"
    >
      <Box w="500px" p={8} bg="gray.50" rounded="md">
        <FormControl mb={8}>
          <FormLabel htmlFor="name">Name</FormLabel>
          <Input
            type="text"
            id="name"
            aria-describedby="john"
            value={name}
            onChange={e => setName(e.target.value)}
          />
        </FormControl>
        <FormControl mb={8}>
          <FormLabel htmlFor="description">Description</FormLabel>
          <Input
            type="text"
            id="description"
            aria-describedby="Description"
            value={description}
            onChange={e => setDescription(e.target.value)}
          />
        </FormControl>
        <FormControl>
          <Button bg="#764ABC" color="white" onClick={handleSubmit}>
            Save
          </Button>
        </FormControl>
      </Box>
    </Box>
  );
};

export default withRouter(AddNewProductForm);
```

In the above form, we’re using the `X-Hasura-User-Id` cookie to send the id of the current user.

Now we just need to import this form in a page.

```jsx
// frontend/pages/products/new.js

import React from "react";

import AddNewProductForm from "../../components/forms/add-new-product";

const CreateProductPage = () => {
  return <AddNewProductForm />;
};

export default CreateProductPage;
```

If we visit [http://localhost:3000/products/new](http://localhost:3000/products/new), we should now be able to see the new form.

![Form to Add New Products](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2020/02/new-products-form.png?resize=730%2C411&ssl=1)

We can also add new products.

![Adding New Products](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2020/02/adding-new-products.png?resize=730%2C411&ssl=1)

In addition, we can view the new product in the ProductsList component.

![Products List](https://i1.wp.com/blog.logrocket.com/wp-content/uploads/2020/02/products-list.png?resize=730%2C411&ssl=1)

## Extending the Next.js app to edit products

What if we want users to be able to edit the products? We can create a form for that too.

```jsx
// frontend/components/forms/edit-product.js

import React, { useState, useEffect } from "react";
import {
  Box,
  Button,
  FormControl,
  FormLabel,
  Input,
  Spinner
} from "@chakra-ui/core";
import { useManualQuery, useMutation } from "graphql-hooks";
import { withRouter } from "next/router";

export const singleProductQuery = `
  query ($id: uuid!){
    product_by_pk (id: $id) {
      id
      name
      description
      created_at
    }
  }
`;

export const updateProductMutation = `
  mutation ($name: bpchar!, $description: bpchar!, $productId: uuid!) {
    update_product(where: {id: {_eq: $productId}}, _set: {name: $name, description: $description}) {
      returning {
        id
        name
        description
      }
    }
  }
`;

const EditProductForm = ({ router }) => {
  const [name, setName] = useState("");
  const [description, setDescription] = useState("");
  const [fetchProduct, { loading, error }] = useManualQuery(singleProductQuery);
  const [updateProduct] = useMutation(updateProductMutation);

  useEffect(() => {
    const fetchProductThenSomething = async () => {
      const product = await fetchProduct({
        skipCache: true,
        variables: {
          id: router.query.productId
        }
      });
      setName(product.data.product_by_pk.name);
      setDescription(product.data.product_by_pk.description);
    };
    fetchProductThenSomething();
  }, []);

  if (loading) {
    return (
      <Box
        w="100%"
        minH="100vh"
        d="flex"
        alignItems="center"
        justifyContent="center"
      >
        <Spinner />
      </Box>
    );
  }

  if (error) {
    return (
      <Box
        w="100%"
        minH="100vh"
        d="flex"
        alignItems="center"
        justifyContent="center"
      >
        <Alert status="error" variant="left-accent">
          <AlertIcon />
          There was an error processing your request
        </Alert>
      </Box>
    );
  }

  const handleSubmit = async e => {
    e.preventDefault();
    await updateProduct({
      variables: {
        name,
        description,
        productId: router.query.productId
      }
    });
  };

  return (
    <Box
      w="100%"
      minH="100vh"
      p={4}
      d="flex"
      alignItems="center"
      justifyContent="center"
    >
      <Box w="500px" p={8} bg="gray.50" rounded="md">
        <FormControl mb={8}>
          <FormLabel htmlFor="name">Name</FormLabel>
          <Input
            type="text"
            id="name"
            aria-describedby="john"
            value={name}
            onChange={e => setName(e.target.value)}
          />
        </FormControl>
        <FormControl mb={8}>
          <FormLabel htmlFor="description">Description</FormLabel>
          <Input
            type="text"
            id="description"
            aria-describedby="Description"
            value={description}
            onChange={e => setDescription(e.target.value)}
          />
        </FormControl>
        <FormControl>
          <Button bg="#764ABC" color="white" onClick={handleSubmit}>
            Save
          </Button>
        </FormControl>
      </Box>
    </Box>
  );
};

export default withRouter(EditProductForm);
```

Let’s render that form in a separate page.

```jsx
// frontend/pages/products/[productId]/edit.js

import React from "react";

import EditProductForm from "../../../components/forms/edit-product";

const EditProductPage = () => {
  return <EditProductForm />;
};

export default EditProductPage;
```

Now, if we visit any Product page (eg: [http://localhost:3000/products/:productId/edit](http://localhost:3000/products/:productId/edit)), we should be able to see the `EditProductForm`.

![Form to Edit a Product](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2020/02/edit-product-form.png?resize=730%2C409&ssl=1)

We can now update any product through this form.

Let’s say we want to make it so that makers can update only the products they created. To do that, we need to define custom permissions for the `Product` model.

![Add Custom Permissions for Updating Products](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2020/02/custom-permission-update-product.png?resize=730%2C408&ssl=1)

We need to define a new Role named `user`. When the `maker_id` equals `X-Hasura-User-Id`, then and only then will the update be allowed.

We still need to make a few changes on our Next.js application to allow makers to edit the products that they created. We just need to update the `SignInForm` and `SignUpForm` components to store the `X-Hasura-User-Role` cookie.

```jsx
// frontend/components/forms/sign-in.js

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

    ...      

    setCookie({}, "X-Hasura-User-Id", data.id, {
        maxAge: 30 * 24 * 60 * 60,
        path: "/"
      });

      setCookie({}, "token", data.token, {
        maxAge: 30 * 24 * 60 * 60,
        path: "/"
      });

      setCookie({}, "X-Hasura-User-Role", data.roles[0], {
        maxAge: 30 * 24 * 60 * 60,
        path: "/"
      });
    }
  };
```

We also need to update the `create` function in the `init-graphql.js` file.

```jsx
// frontend/lib/init-graphql.js

function create(initialState = {}) {
  return new GraphQLClient({
    ssrMode: typeof window === "undefined",
    url: "http://localhost:8080/v1/graphql",
    cache: memCache({ initialState }),
    fetch: typeof window !== "undefined" ? fetch.bind() : unfetch, // eslint-disable-line
    headers: {
      "x-hasura-admin-secret": "secret",
      "x-hasura-role": parseCookies()["X-Hasura-User-Role"],
      "X-Hasura-User-Id": parseCookies()["X-Hasura-User-Id"]
    }
  });
}
```

If we log in again, we should be able to see three cookies: `X-Hasura-User-Id`, `X-Hasura-User-Role`, and `token`.

![Storing All Necessary Cookies in the Browser](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2020/02/storing-cookies-in-browser.png?resize=730%2C412&ssl=1)

If a user visits any product page, they will be able to update only the products they created.

## Conclusion

It’s possible to [use GraphQL Subscriptions with Hasura](https://docs.hasura.io/1.0/graphql/manual/subscriptions/index.html). You can also [trigger webhooks on database events](https://hasura.io/event-triggers) using Hasura.

You should now have a basic understanding of how to use Hasura with Next.js. The whole project is available on [GitHub](https://github.com/ghoshnirmalya/product-hunt-clone-using-nextjs-and-hasura) for your reference.

Note that the applications we built are not yet ready for production. See the [Hasura documentation](https://docs.hasura.io/1.0/graphql/manual/deployment/index.html) for more information about deploying Hasura applications. Likewise, you can read more about deploying Next.js applications in the [Next.js documentation](https://nextjs.org/learn/basics/deploying-a-nextjs-app).

* * * 
## Plug: [LogRocket](https://logrocket.com/signup/), a DVR for web apps
 
![LogRocket Dashboard Free Trial Banner](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2017/03/1d0cd-1s_rmyo6nbrasp-xtvbaxfg.png?resize=1200%2C677&ssl=1)
 
[LogRocket](https://logrocket.com/signup/) is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
 
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.
 
[Try it for free](https://logrocket.com/signup/).
* * *

The post [Building a Product Hunt clone app using Hasura and Next.js](https://blog.logrocket.com/building-a-product-hunt-clone-app-using-hasura-and-next-js/) appeared first on [LogRocket Blog](https://blog.logrocket.com).