Full Stack Quiz Game with NextJS - My Journey
Tags: javascript, webdev, react, nextjs
Published: 2020-11-20 20:10:01

## Intro

After being a **ReactJS** developer for a couple of years now (only on the client-side), I started to feel the need to discover and understand which fullstack solutions are currently available.

One that really stands out is **NextJS**. It has an incredible 56.9K of GitHub stars. In my opinion, the best way to learn a framework or technology is by creating an example application.

That's how the **Quiz Game** project has born. By any means, this is an exhaustive tutorial, preferably an overview of the project's steps I've used to create the game.

Main libs used in this project:
* [Semantic UI React](https://react.semantic-ui.com/) - React Components lib (these have some custom style override with CSS Modules). 
* [React Toastify](https://github.com/fkhadra/react-toastify) - Lib used to add notifications in UI.
* [@artsy/fresnel](https://github.com/artsy/fresnel) - Lib to create media queries components in JSX.
* [react-query](https://github.com/tannerlinsley/react-query) - Lib used to manage async requests made on the client-side.
* [react-hook-form](https://react-hook-form.com/) - React lib used for forms management.

**TL;DR**:
* [Github](https://github.com/hlopes/quiz-game) - project source code
* [Demo](https://quiz-game.hlopes.vercel.app/) - application in production

---

## What is NextJS

**NextJS** is an opinionated framework made by **Vercel** built on the top of _NodeJS_, _Webpack_, _Babel_, and _ReactJS_.

This framework doesn't require additional configuration to have an optimized application for production. The hybrid approach for rendering is another of the main advantages. The decision between static generation (**SG**) and server-side rendering (**SSR**) are supported on a per-page basis.

---

## Quiz Game

The idea for this app/game has come up after encountering the **Open Trivia API** available [here](https://opentdb.com/api_config.php). In my mind, I began to see a small application divided into the following sections:

* Homepage with the **_Top 10_** players;
* Authentication pages for **_Login_** and **_Register_**;
* **_Game_** page;
* **_Account_** page will display players statistics and game settings;
* **_About_** page will display the parsing result of the README.md file.

 All these should take into account a responsive layout. But first things first.

---

## Project Setup

#### 1 - Project creation

The easiest way to get started is by using the CLI tool [create-next-app](https://nextjs.org/docs/api-reference/create-next-app), that will set up everything for you:

```
npx create-next-app quiz-game
# or
yarn create next-app quiz-game
```
At the time of this article, the versions used for **Next** was `10.0.1` and for **React** `17.0.1`.

#### 2 - MongoDB Configuration

I opt to use MongoDB to store the application data, mainly because I never used it professionally. The simple way to start using this NoSQL database is to create an account and a new cluster in [MongoDB Cloud Atlas](https://www.mongodb.com/cloud/atlas). 

I've created a cluster named `quiz-game` and change the built-in role to _"Read and write to any database"_.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/2kuqx1koiqnxodbndoes.jpg)

Additionally, I had to insert o `0.0.0.0/0` in the IP Address field in the _"Network Access"_ section. That will allow connecting to your cluster from anywhere.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/itigcthldp5e31x7pgo3.jpg)

---

## Backend

#### 1 - API routes

The server logic of the application is mainly based on the [API routes](https://nextjs.org/docs/api-routes/introduction) feature of **NextJS**. 

> Any file inside the folder `pages/api` is mapped to `/api/*` and will be treated as an API endpoint instead of a page. They are server-side only bundles and won't increase your client-side bundle size.

For example, the following function is executed when a new user tries to register:

```javascript
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';

import { connectToDatabase } from '../../utils/mongodb';
import errors from '../../utils/errors';

const handler = async (req, res) => {
    const { name, email, password, image, isExternal } = JSON.parse(req.body);

    if (!name || !email || !password) {
        res.statusCode = 422;

        return res.json({ ...errors.REGISTER_FORM_DATA_MISSING });
    }

    try {
        const { db } = await connectToDatabase();

        const savedUser = await db.collection('users').findOne({ email });

        if (!process.env.JWT_SECRET) {
            res.statusCode = 422;

            return res.json({ ...errors.SECRET_NOT_DEFINED });
        }

        if (savedUser && !isExternal) {
            res.statusCode = 422;

            return res.json({ ...errors.ALREADY_REGISTERED });
        }

        const hashed = await bcrypt.hash(password, 12);

        if (hashed) {
            if (savedUser) {
                await db
                    .collection('users')
                    .updateOne({ email }, { $set: { password } });

                const token = jwt.sign(
                    { _id: savedUser._id },
                    process.env.JWT_SECRET
                );

                return res.json({
                    message: 'Saved successfully',
                    user: savedUser,
                    token,
                });
            }

            const user = {
                email,
                name,
                password: hashed,
                image,
                points: 0,
                questionsAnswered: 0,
            };

            await db.collection('users').insertOne(user);

            const foundUser = await db.collection('users').findOne({ email });

            await db.collection('preferences').insertOne({
                user: foundUser,
                numQuestions: 3,
                gender: '',
            });

            const token = jwt.sign({ _id: user._id }, process.env.JWT_SECRET);

            res.status(201);

            return res.json({
                message: 'Saved successfully',
                user,
                token,
            });
        }
    } catch (error) {
        res.statusCode = 500;

        return res.json({ ...errors.ERROR_REGISTERING });
    }
};

export default handler;
```

After passing the initial validation of the required arguments, I get the DB connection from the `connectToDatabase` (this will return a cached connection if it was already created) to check if a user with the same email was already inserted. The next step consists of creating a hash (with [bcrypt](https://github.com/kelektiv/node.bcrypt.js#readme)) for the password and signing a token with the user's id and the secret (with JWT) stored in the environment variables file.

I created the `.env.local` file on the root of the project and added the following var:

```properties
JWT_SECRET={your_secret}
```
Down below are the description of all application API endpoints:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/vpejy801p5rsgmeta4kj.jpg)

* `auth/[...nextauth].js` - Several dynamic endpoints related to external authentication providers such as Google, Facebook etc.

* `preferences/[userid].js` - Dynamic endpoint to fetch the previous preferences saved by the user.

* `preferences/index.js` - Endpoint to store preferences saved by the user.

* `login` - Endpoint to sign in an existing user.

* `register` - Already described above.

* `score` - Endpoint to store the player score at the end of each game.

#### 2 - MongoDB connection

Regarding the Mongo DB connection, I've chosen the utility function available in the **NextJS** example with MongoDB in [here](https://github.com/vercel/next.js/blob/canary/examples/with-mongodb/util/mongodb.js). The exported function returns the same single instance of the DB connection for each request, avoiding creating unnecessary multiple connections.
Finally, I needed to add the project's environment variables:

```properties
MONGODB_URI=mongodb+srv://{your_connection_string}?retryWrites=true
MONGODB_DB={your_db_name}
```
If you have any difficulties getting the database connection string, check this [video](https://www.youtube.com/watch?v=Ej05tq1220A).

---

## Pages

The application is segmented into the following pages:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/7wulbxkhehcs353uvglw.jpg)

* `about` - About page is the parsing result of the project readme file.

* `account` - User's account area.

* `game` - The entry point for the new game and final score.

* `register` - Registration for new users that choose not to use a social network authentication.

* `signin` - Login form and social networks authentication.

* `index.js` - Home page with **Top 10** players.

#### 1 - SSR Example - Homepage

The main concern of this page is to retrieve the data of the **Top 10** players.  This should be done before the first render. It doesn't require the user to be logged in.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/p2eed822tyhlm0kgdd3z.jpg)

For me, this is a nice candidate to use **SSR** in **NextJS**. This means that the HTML is generated for each request.

Having said that, here is the code for the Home page component:

```javascript
import React from 'react';
import PropTypes from 'prop-types';
import {
    Label,
    Header,
    Segment,
    Table,
    Image,
    Divider,
} from 'semantic-ui-react';
import isEmpty from 'lodash/isEmpty';

import getAvatar from '../utils/getAvatar';
import { connectToDatabase } from '../utils/mongodb';
import Layout from '../components/layout/Layout';
import useBreakpoints from '../common/useBreakpoints';

const Home = ({ top }) => {
    const { lteSmall } = useBreakpoints();

    return (
        <Layout>
            <Segment raised padded={lteSmall ? true : 'very'}>
                <Header as="h2">Welcome to Quiz Game</Header>
                <p>This is just a game built with NextJS.</p>
                <br />
                <Divider />
                {!isEmpty(top) ? (
                    <>
                        <Header as="h3">Top 10</Header>
                        <Table
                            basic="very"
                            celled
                            collapsing
                            unstackable
                            striped
                        >
                            <Table.Header>
                                <Table.Row>
                                    <Table.HeaderCell>Player</Table.HeaderCell>
                                    <Table.HeaderCell>Score</Table.HeaderCell>
                                    <Table.HeaderCell>
                                        Questions
                                    </Table.HeaderCell>
                                </Table.Row>
                            </Table.Header>
                            <Table.Body>
                                {top?.map((player, index) => (
                                    <Table.Row key={index}>
                                        <Table.Cell>
                                            <Header as="h4" image>
                                                {player?.user?.image ? (
                                                    <Image
                                                        alt={player?.user?.name}
                                                        src={
                                                            player?.user?.image
                                                        }
                                                        rounded
                                                        size="mini"
                                                    />
                                                ) : (
                                                    <Image
                                                        alt={player?.user?.name}
                                                        src={getRandomAvatar(
                                                            player?.gender
                                                        )}
                                                        rounded
                                                        size="mini"
                                                    />
                                                )}
                                                {player?.user?.name}
                                            </Header>
                                        </Table.Cell>
                                        <Table.Cell textAlign="right">
                                            {player?.user?.points > 0 ? (
                                                <Label color="blue">
                                                    {player?.user?.points}
                                                </Label>
                                            ) : (
                                                <Label color="yellow">
                                                    {player?.user?.points}
                                                </Label>
                                            )}
                                        </Table.Cell>
                                        <Table.Cell textAlign="right">
                                            {player?.user?.questionsAnswered}
                                        </Table.Cell>
                                    </Table.Row>
                                ))}
                            </Table.Body>
                        </Table>
                    </>
                ) : null}
            </Segment>
        </Layout>
    );
};

Home.propTypes = {
    top: PropTypes.array,
};

export default Home;

export async function getServerSideProps() {
    const { db } = await connectToDatabase();

    const usersWithPreferences = await db
        .collection('preferences')
        .find()
        .limit(10)
        .sort({ ['user.points']: -1 })
        .toArray();

    return {
        props: {
            top: JSON.parse(JSON.stringify(usersWithPreferences)),
        },
    };
}
```
The main goal here is to display the list of players with a higher score. This information is available in the prop `top` ( they needed to be previously registered or signed in with social networks authentication). The magic here is that the async function `getServerSideProps` will return the `top` before the first render of this page on the server-side. Internally I'm not doing more than get the DB connection and find the list of users with the score sorted by the score descending. For more info, please press this [link](https://nextjs.org/docs/basic-features/data-fetching#getserversideprops-server-side-rendering).

#### 2 - SG Example - About

The biggest difference between **SSR** and **SG** in **NextJS** is that **SG** mode will pre-render this page at build time using the props returned by `getStaticProps`. This means that the same HTML is served for each request.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/olwlbrm2nlq33o7o96pm.jpg)

For example, check the code of the About page component:

```javascript
import React from 'react';
import PropTypes from 'prop-types';
import { Segment } from 'semantic-ui-react';

import getAboutData from '../../lib/about';
import useBreakpoints from '../../common/useBreakpoints';
import Layout from '../../components/layout/Layout';

import styles from './About.module.css';

const About = ({ aboutData }) => {
    const { lteSmall } = useBreakpoints();

    return (
        <Layout>
            <Segment
                raised
                padded={lteSmall ? true : 'very'}
                className={styles.wrapper}
            >
                <div dangerouslySetInnerHTML={{ __html: aboutData }} />
            </Segment>
        </Layout>
    );
};

export async function getStaticProps() {
    const aboutData = await getAboutData();

    return {
        props: {
            aboutData,
        },
    };
}

About.propTypes = {
    aboutData: PropTypes.string,
};

export default About;

```

This is another page that doesn't require the user to be logged in. The only thing is required before the render,  is the parsing data from the markdown of the **README.md** file. The final content of the page won't change for any user or page request (static). So with this in mind, the `getStaticProps` function is used to pass the prop `aboutData` with the output from the `lib/about.js` file.

```javascript
import fs from 'fs';
import path from 'path';

import matter from 'gray-matter';
import remark from 'remark';
import html from 'remark-html';

const readmeDir = path.join(process.cwd());

export default async function getAboutData() {
    const fullPath = path.join(readmeDir, `README.md`);
    const fileContents = fs.readFileSync(fullPath, 'utf8');

    // Use gray-matter to parse the post metadata section
    const matterResult = matter(fileContents);

    // Use remark to convert markdown into HTML string
    const processedContent = await remark()
        .use(html)
        .process(matterResult.content);

    return processedContent.toString();
}
```
#### 3 - CSR Example - All remaining pages

Except for the **Home** and **About** page, the remaining pages depended on the user session status verification. The **Game** and **Account** will require the user to be logged in. On the other hand, the authentication pages like **Login** and **Register** should be prevented if the user has already signed up. Because of this particular question, the **Game** and **Account** content are only rendered on the client side.

---

### Deploy to Vercel

After all the development phases, surprisingly the easiest task was the app deployment through **Vercel** (also the company's name behind **NextJs**). For the sake of brevity, that guide can be consulted [here](https://nextjs.org/docs/deployment). In this platform, you can check the build/function logs and also some cool features such as the recent addition of Analytics (limited for free accounts).

---

### Lighthouse

One of the major promised benefits of using **NextJS** is it's performance and SEO optimization. These were the results of lighthouse accomplished with the live app in Vercel:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/2nz0h5sr4r55kccg679z.jpg)

---

### Conclusion

There is no doubt that **NextJS** is a great framework to create a full-stack application from the scratch. Everything will be already configured and optimized in terms of code splitting and bundling for production. There is a lot of things that you don't need to bother about. But that doesn't mean we cannot easily extend for a more tailored solution. For those with a background in **React**, the learning curve is minimal.
Development with this framework is fast and refreshing. I strongly recommend at least to try it.

All comments are welcome, thanks.