Next.js and MongoDB full-fledged app Part 1: User authentication (using Passport.js)
Tags: javascript, react, mongodb, tutorial
Published: 2020-03-26 15:29:10

`nextjs-mongodb-app` is a full-fledged app built with Next.js and MongoDB. Today, I will add our fundamental feature: User Authentication.

Below are the Github repository and a demo for this project to follow along.

[Github repo](https://github.com/hoangvvo/nextjs-mongodb-app "nextjs mongodb app")

[Demo](https://nextjs-mongodb.now.sh/)

## About `nextjs-mongodb-app` project

> nextjs-mongodb-app is a Full-fledged serverless app made with Next.JS and MongoDB

Different from many other Next.js tutorials, this:

- Does not use the enormously big Express.js, supports `serverless`
- Minimal, no fancy stuff like Redux or GraphQL for simplicity in learning
- Using Next.js latest features like API Routes or getServerSideProps

For more information, visit the [Github repo](https://github.com/hoangvvo/nextjs-mongodb-app "nextjs mongodb app").

## Getting started

### Environmental variables

The environment variables should should be placed in `.env.local`.
See [Environment variables](https://nextjs.org/docs/basic-features/environment-variables).

Required environmental variables for now includes:

- process.env.**MONGODB_URI**

### Validation library

I'm using [validator](https://github.com/validatorjs/validator.js) for email validation, but feel free to use your library or write your check.

I'm also using [ajv](https://ajv.js.org/) to validate the incoming request body.

### Password hashing library

Password must be hashed. Period. There are different libraries out there:

- [bcrypt](https://www.npmjs.com/package/bcrypt) (most common)
- [argon2](https://www.npmjs.com/package/argon2) (recommended)

## Middleware

You may be familiar with the term _middleware_ if you have an `ExpressJS` background.

We can use Middleware in Next.js by using [`next-connect`](https://github.com/hoangvvo/next-connect) with the familiar `.use()` syntax. Beside middleware, `next-connect` also allows us to do method routing via `.get()`, `.post()`, etc., so we don't have to write manual `if (req.method)` checks.

You can even continue with this project without `next-connect` using the guide [API Middlewares](https://nextjs.org/docs/api-routes/api-middlewares), but it might require more code.

### Database middleware

We will need to have a middleware that handles the database connection.

```javascript
import { MongoClient } from "mongodb";

/**
 * Global is used here to maintain a cached connection across hot reloads
 * in development. This prevents connections growing exponentiatlly
 * during API Route usage.
 * https://github.com/vercel/next.js/pull/17666
 */
global.mongo = global.mongo || {};

export async function getMongoClient() {
  if (!global.mongo.client) {
    global.mongo.client = new MongoClient(process.env.MONGODB_URI);
  }
  // It is okay to call connect() even if it is connected
  // using node-mongodb-native v4 (it will be no-op)
  // See: https://github.com/mongodb/node-mongodb-native/blob/4.0/docs/CHANGES_4.0.0.md
  await global.mongo.client.connect();
  return global.mongo.client;
}

export default async function database(req, res, next) {
  if (!global.mongo.client) {
    global.mongo.client = new MongoClient(process.env.MONGODB_URI);
  }
  req.dbClient = await getMongoClient();
  req.db = req.dbClient.db(); // this use the database specified in the MONGODB_URI (after the "/")
  if (!indexesCreated) await createIndexes(req.db);
  return next();
}
```

I then attach the database to `req.db`. In this middleware, we first create a "cachable" MongoClient instance if it does not exist. This allows us to work around a common issue in serverless environments where redundant MongoClients and connections are created.

The approach used in this project is to use the middleware function `database` to attach the client to `req.dbClient` and the database to `req.db`. However, as an alternative, the `getMongoClient()` function can also be used to get a client anywhere (this is the approach used by the official Next.js example and shown MongoDB blog - We choose to use a middleware instead).

### Session middleware

\*An earlier version of this project uses [express-session](https://github.com/expressjs/session), but this has been replaced with [next-session](https://github.com/hoangvvo/next-session) due its incompatibility with Next.js 11+.

For session management, Redis or Memcached are better solutions, but since we are already using MongoDB, we will use [connect-mongo](https://github.com/jdesboeufs/connect-mongo).

We create the session middleware as below (consult [next-session documentation](https://github.com/hoangvvo/next-session) for more detail):

```javascript
import MongoStore from "connect-mongo";
import { getMongoClient } from "./database";

const mongoStore = MongoStore.create({
  clientPromise: getMongoClient(),
  stringify: false,
});

const getSession = nextSession({
  store: promisifyStore(mongoStore),
  cookie: {
    httpOnly: true,
    secure: process.env.NODE_ENV === "production",
    maxAge: 2 * 7 * 24 * 60 * 60, // 2 weeks,
    path: "/",
    sameSite: "strict",
  },
  touchAfter: 1 * 7 * 24 * 60 * 60, // 1 week
});

export default async function session(req, res, next) {
  await getSession(req, res);
  next();
}
```

### Email/Password authentication using Passport.js

We will use [Passport.js](http://www.passportjs.org/) for authentication.

We will initialize our Passport instance.

```javascript
import passport from "passport";
import bcrypt from "bcryptjs";
import { Strategy as LocalStrategy } from "passport-local";
import { ObjectId } from "mongodb";

passport.serializeUser((user, done) => {
  done(null, user._id.toString());
});

passport.deserializeUser((req, id, done) => {
  req.db
    .collection("users")
    .findOne({ _id: new ObjectId(id) })
    .then((user) => done(null, user));
});

passport.use(
  new LocalStrategy(
    { usernameField: "email", passReqToCallback: true },
    async (req, email, password, done) => {
      const user = await req.db.collection("users").findOne({ email });
      if (user && (await bcrypt.compare(password, user.password)))
        done(null, user);
      else done(null, false);
    }
  )
);

export default passport;
```

Our `passport.serializeUser` function will serialize the user id into our session. Later we will use that same id to get our user object in `passport.deserializeUser`. The reason we have to pass it into `ObjectId` is because our `_id` in MongoDB collection is of such type, while the serialized `_id` is of type `string`.

We use [passport-local](https://github.com/jaredhanson/passport-local) for email/password authentication. We first find the user using the email `req.db.collection('users').findOne({ email })` (`req.db` is available via [database middleware](#database-middleware)). Then, we compare the password `await bcrypt.compare(password, user.password)`. If everything matches up, we resolve the user via `done(null, user)`.

### Authentication middleware

In order to authenticate users, we need three seperate middleware: Our above `session`, `passport.initialize()` and `passport.session()`middleware. `passport.initialize()` initialize Passport.js, and `passport.session()` will authenticate user using `req.session` which is provided by `session`.

```javascript
const handler = nc();
handler.use(session, passport.initialize(), passport.session());
handler.get(foo);
handler.post(bar);
```

However, to avoid retyping the same `.use()` or leaving any of them out, I grouped three of them into an array:

```javascript
export const auths = [session, passport.initialize(), passport.session()];
```

and use it like below:

```javascript
import { auths } from "@/api-lib/middlewares";

const handler = nc();
handler.use(...auths); // this syntax spread out the three middleware and is equivalent to the original version
```

### Request body validation middleware

It is a good practice to always validate incoming request bodies. Here we write a middleware that validates `req.body` using `ajv`.

```js
import Ajv from "ajv";

export function validateBody(schema) {
  const ajv = new Ajv();
  const validate = ajv.compile(schema);
  return (req, res, next) => {
    const valid = validate(req.body);
    if (valid) {
      return next();
    } else {
      const error = validate.errors[0];
      return res.status(400).json({
        error: {
          message: `"${error.instancePath.substring(1)}" ${error.message}`,
        },
      });
    }
  };
}
```

The function takes in a JSON schema, creates a Ajv validate function, and returns a middleware that makes use of it. The middleware would validate `req.body` and if there is an error, we immediately return the error with status code 400.

## User state management

### Endpoint to get the current user

Let's have an endpoint that fetches the current user. I will have it in `/api/user`.

In `/api/user/index.js`, put in the following content:

```javascript
import nc from "next-connect";
import { database, auths } from "@/api-lib/middlewares";

const handler = nc();
handler.use(database, ...auths);
handler.get(async (req, res) => res.json({ user: req.user }));

export default handler;
```

We simply return `req.user`, which is populated by our `auths` middleware. However, there is a problem. `req.user` is the whole `user` document, which includes the `password` field.

To fix that, we use a MongoDB feature called [Projection](https://docs.mongodb.com/manual/tutorial/project-fields-from-query-results/) to filter it out. We made one adjustment to the Passport deserialize function:

```javascript
passport.deserializeUser((req, id, done) => {
  req.db
    .collection("users")
    .findOne({ _id: new ObjectId(id) }, { projection: { password: 0 } })
    .then((user) => done(null, user));
});
```

### State management using `swr`

> SWR is a React Hooks library for remote data fetching.

We will use [`swr`](https://swr.now.sh/) for state management. I understand basic understandings of `swr`, but you can always read [its documentation](https://swr.vercel.app/docs/getting-started).

We first define a fetcher function:

```javascript
export const fetcher = (...args) => {
  return fetch(...args).then(async (res) => {
    let payload;
    try {
      if (res.status === 204) return null; // 204 does not have body
      payload = await res.json();
    } catch (e) {
      /* noop */
    }
    if (res.ok) {
      return payload;
    } else {
      return Promise.reject(payload.error || new Error("Something went wrong"));
    }
  });
};
```

This function is an augmentation of [fetch](https://developer.mozilla.org/en-US/docs/Web/API/fetch) (we actually forward all arguments to it). After receiving a response. We will try to parse it as JSON using [res.json](https://developer.mozilla.org/en-US/docs/Web/API/Response/json). Since `fetch` does not throw if the request is 4xx, we will check `res.ok` (which is `false` if `res.status` is 4xx or 5xx) and manually reject the promise using `payload.error`.

The reason I return `payload.error` is because I intend to write my API to return the error as:

```json
{
  "error": {
    "message": "some message"
  }
}
```

If for some reason, the error payload is not like that, we return a generic "Something went wrong".

#### useCurrentUser hook

We need a useSWR hook to return our current user:

```javascript
import useSWR from "swr";

export function useCurrentUser() {
  return useSWR("/api/user", fetcher);
}
```

`useSWR` will use our `fetcher` function to fetch `/api/user`.

To visualize, the result from `/api/user` (which we will write in a later section) is in this format:

```json
{
  "user": {
    "username": "jane",
    "name": "Jane Doe",
    "email": "jane@example.com"
  }
}
```

This will be the value of `data`. Thus, we get the `user` object by `const user = data && data.user`.

Now, whenever we need to get our user information, we simply need to use `useUser`.

```javascript
const [user, { mutate }] = useCurrentUser();
```

Our `mutate` function can be used to update the user state. For example:

```javascript
const { data: { user } = {} } = useCurrentUser();
```

Since `data` is `undefined` initially, I default it to `= {}` to avoid the `Uncaught TypeError: Cannot read property of undefined` error.

## User registration

Let's start with the user registration since we need at least a user to work with.

![Sign Up Page](https://hoangvvo.com/blog/next-js-and-mongodb-app-1/sign-up-page.png)

### Building the Signup API

Let's say we sign the user up by making a `POST` request to `/api/users` with a name, a username, an email, and a password.

Let's create `/api/users/index.js`:

```javascript
import { ValidateProps } from "@/api-lib/constants";
import { database, validateBody } from "@/api-lib/middlewares";
import nc from "next-connect";
import isEmail from "validator/lib/isEmail";
import normalizeEmail from "validator/lib/normalizeEmail";
import slug from 'slug';

const handler = nc();

handler.use(database); // we don't need auths in this case because we don't do authentication

// POST /api/users
handler.post(
  validateBody({
    type: "object",
    properties: {
      username: { type: "string", minLength: 4, maxLength: 20 },
      name: { type: "string", minLength: 1, maxLength: 50 },
      password: { type: "string", minLength: 8 },
      email: { type: "string", minLength: 1 },
    },
    required: ["username", "name", "password", "email"],
    additionalProperties: false,
  }),
  async (req, res) => {
    const { name, password } = req.body;
    const username = slug(req.body.username);
    const email = normalizeEmail(req.body.email); // this is to handle things like jane.doe@gmail.com and janedoe@gmail.com being the same
    if (!isEmail(email)) {
      res.status(400).send("The email you entered is invalid.");
      return;
    }
    // check if email existed
    if ((await req.db.collection("users").countDocuments({ email })) > 0) {
      res.status(403).send("The email has already been used.");
    }
    // check if username existed
    if ((await req.db.collection("users").countDocuments({ username })) > 0) {
      res.status(403).send("The username has already been taken.");
    }
    const hashedPassword = await bcrypt.hash(password, 10);

    const user = {
      emailVerified: false,
      profilePicture,
      email,
      name,
      username,
      bio,
    };

    const password = await bcrypt.hash(originalPassword, 10);

    const { insertedId } = await db
      .collection("users")
      // notice how I pass the password independently and not right into the user object (to avoid returning the password later)
      .insertOne({ ...user, password });

    user._id = insertedId; // we attach the inserted id (we don't know beforehand) to the user object

    req.logIn(user, (err) => {
      if (err) throw err;
      // when we finally log in, return the (filtered) user object
      res.status(201).json({
        user,
      });
    });
  }
);

export default handler;
```

The handler:

- is passed through our request body validation
- normalize and validates the email
- slugify the username using the [slug](https://www.npmjs.com/package/slug) package (since we don't want some usernames to be like "unicode ♥ is ☢")
- Check if the email existed by counting its # of occurance `req.db.collection('users').countDocuments({ email })`
- Check if the username existed by counting its # of occurance `req.db.collection('users').countDocuments({ username })`
- hash the password `bcrypt.hash(password, 10)`
- insert the user into our database.

After that, we log the user in using `passport`'s [`req.logIn`](http://www.passportjs.org/docs/login/).

If the user is authenticated, I return our user object.

### `pages/sign-up.jsx`: The sign up page

In `sign-up.jsx`, we will have the following content:

```jsx
import { fetcher } from "@/lib/fetch";
import { useCurrentUser } from "@/lib/user";
import Link from "next/link";
import { useRouter } from "next/router";
import { useCallback, useRef, useState } from "react";
import toast from "react-hot-toast";

const SignupPage = () => {
  const emailRef = useRef();
  const passwordRef = useRef();
  const usernameRef = useRef();
  const nameRef = useRef();

  const { mutate } = useCurrentUser();

  const onSubmit = useCallback(
    async (e) => {
      e.preventDefault();
      try {
        const response = await fetcher("/api/users", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            email: emailRef.current.value,
            name: nameRef.current.value,
            password: passwordRef.current.value,
            username: usernameRef.current.value,
          }),
        });
        mutate({ user: response.user }, false);
        router.replace("/feed");
      } catch (e) {
        console.error(e.message);
      }
    },
    [mutate, router]
  );

  return (
    <>
      <Head>
        <title>Sign up</title>
      </Head>
      <div>
        <h2>Sign up</h2>
        <form onSubmit={onSubmit}>
          <input ref={emailRef} type="email" placeholder="Email Address" />
          <input
            ref={emailRef}
            type="password"
            autoComplete="new-password"
            placeholder="Password"
          />
          <input
            ref={usernameRef}
            autoComplete="username"
            placeholder="Username"
          />
          <input
            ref={usernameRef}
            autoComplete="name"
            placeholder="Your name"
          />
          <button type="submit">Sign up</button>
        </form>
      </div>
    </>
  );
};

export default SignupPage;
```

What `onSubmit` does is to make a `POST` request to `/api/users` with our `email`, `password`, `username`, `name`. I use `ref` to grab the values from the [uncontrolled inputs](https://reactjs.org/docs/uncontrolled-components.html).

If the request comes back successfully, we use SWR `mutate` to update the current user cache then use `router` to navigate to the main page.

## User authentication

Now that we have one user. Let's try to authenticate the user. (We actually _did_ authenticate the user when he or she signs up)

![Login Page](https://hoangvvo.com/blog/next-js-and-mongodb-app-1/login-page.png)

Let's see how we can do it in `/login`, where we make a `POST` request to `/api/auth`.

### Building the Authentication API

Let's create `api/auth.js`:

```javascript
import { passport } from "@/api-lib/auth";
import nc from "next-connect";
import { auths, database } from "@/api-lib/middlewares";

const handler = nc();

handler.use(database, ...auths);

handler.post(passport.authenticate("local"), (req, res) => {
  res.json({ user: req.user });
});

export default handler;
```

When a user makes a POST request to `/api/auth`, we simply call the previously set-up `passport.authenticate` to sign the user in based on the provided `email` and `password`.

If the credential is valid, `req.user`, our user object, will be returned with a **200** status code.

Otherwise, `passport.authenticate` will returns a `401 unauthenticated`.

### `pages/login.jsx`: The login page

Here is our code for `pages/login.jsx`:

```jsx
import { useCallback, useEffect } from "react";
import Head from "next/head";
import Link from "next/link";
import { useRouter } from "next/router";
import { useCallback, useEffect, useRef } from "react";
import { useCurrentUser } from "@/lib/user";

const LoginPage = () => {
  const emailRef = useRef();
  const passwordRef = useRef();

  const { data: { user } = {}, mutate, isValidating } = useCurrentUser();
  const router = useRouter();
  useEffect(() => {
    if (isValidating) return;
    if (user) router.replace("/feed");
  }, [user, router, isValidating]);

  const onSubmit = useCallback(
    async (event) => {
      event.preventDefault();
      try {
        const response = await fetcher("/api/auth", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            email: emailRef.current.value,
            password: passwordRef.current.value,
          }),
        });
        mutate({ user: response.user }, false);
      } catch (e) {
        console.error(e);
      }
    },
    [mutate]
  );

  return (
    <>
      <Head>
        <title>Sign in</title>
      </Head>
      <h2>Sign in</h2>
      <form onSubmit={onSubmit}>
        <input
          ref={emailRef}
          id="email"
          type="email"
          name="email"
          placeholder="Email address"
          autoComplete="email"
        />
        <input
          ref={passwordRef}
          id="password"
          type="password"
          name="password"
          placeholder="Password"
          autoComplete="current-password"
        />
        <button type="submit">Sign in</button>
      </form>
    </>
  );
};

export default LoginPage;
```

The idea is the same, we grab the values from the inputs and submit our requests to `/api/auth`. We will update the SWR cache using `mutate` if the response is successful.

I also set up a `useEffect` that automatically redirects the user as soon as the SWR cache returns a user.

### Sign out

Let's add functionality to the **Sign out** button, which will generally be on our `Navbar`:

```jsx
import { useCallback } from "react";
import { useCurrentUser } from "@/lib/user";

const Navbar = () => {
  const { data: { user } = {}, mutate } = useCurrentUser();

  const onSignOut = useCallback(async () => {
    try {
      await fetcher("/api/auth", {
        method: "DELETE",
      });
      mutate({ user: null });
    } catch (e) {
      toast.error(e.message);
    }
  }, [mutate]);

  return (
    /* ... */
    <button onClick={onSignOut}>Sign out</button>
    /* ... */
  );
};
```

We make a `DELETE` request to `/api/auth`, and if it is successful, we update the SWR cache using `mutate`.

The last part is to write a `DELETE` request handler in `api/auth.js`:

```javascript
handler.delete(async (req, res) => {
  await req.session.destroy();
  // or use req.logOut();
  res.status(204).end();
});
```

## Conclusion

Alright, let's run our app and test it out. This will be the first step in building a full-fledged app using **Next.js** and **MongoDB**.

I hope this can be a boilerplate to launch your next great app. Again, check out the repository [nextjs-mongodb-app](https://github.com/hoangvvo/nextjs-mongodb-app). If you find this helpful, consider staring the repo to motivate me with development.

Good luck on your next Next.js + MongoDB project!