How to build a simple login with Nextjs? (and react hooks)
Tags: react, beginners, javascript, webdev
Published: 2020-02-29 21:09:46

This post is in no way endorsed by [Zeit](https://zeit.co/) but a big shoutout to those guys because what they are building is amazing. As close as you can get to wizardry and superpowers (with Javascript at least).

I find the Next.JS framework to be amazingly simple and fast to learn. The documentation is great itself and they even have provided a learning site. Please do check it out.

You can review the full code in my repo: 
[https://github.com/mgranados/simple-login](https://github.com/mgranados/simple-login)

And the final product, that uses this login with some improvements you can find it over here: [Booktalk.io](https://booktalk.io/) A page for sharing book reviews inspired heavily on Hacker News as you could notice. I will provide more intel on how to create more features and the full project on upcoming posts. [Follow me](https://twitter.com/martingranadosg) if you are interested!

## The setup 🛠

You need to have Node +10 installed and yarn or npm. I personally prefer `yarn` and will be using that through the tutorial but npm is perfectly fine as well. [Commands are a bit different](https://classic.yarnpkg.com/en/docs/migrating-from-npm/), that's it.


## Create a Nextjs app

As per Next.js team recommendation the preferred way to do this is:
```bash
yarn create next-app
```
(Assuming you have [Node](https://nodejs.org/en/) and [Yarn](https://yarnpkg.com/) installed)

That will create a folder structure that will look like this:
![Next folder structure](https://www.dropbox.com/s/dinlx9u4e18247g/Screen%20Shot%202020-02-26%20at%208.28.58.png?raw=1)


## The local development with Nextjs

That's it! You got it alright. Now to get to test the app you can run 

```bash
yarn dev
```

And that should fire up the next dev build and expose a dev version on your `http://localhost:3000/`.

## Let's build the API! 🏗

Now for starting crafting the API on NextJS 9.2 you can add a folder `/pages/api` and everything that you build there would be exposed as a serverless function when building for production in things like [Now](https://github.com/zeit/now). How magical that is!?

Something quite interesting here is that you can use ES6 and things like `import` instead of `require` as you would in a NodeJS file using `CommonJS`

Let's build the relevant endpoints for a login:
- `POST /users` According to REST principles this is the preferred way to created a `user` resource. Which can be translated to: creating a user. Which is what happens when someone signs up.
- `POST /auth` This is a personal preference of mine for naming the endpoint that the frontend hits when the users logins.
- `GET /me` This is also a personal preference for the endpoint that will get hit and retrieve the user data if it is logged in correctly.

Let's get to it

### POST /users

The first part of the file is devoted to importing relevant libraries and creating a connection to the DB.

`/pages/api/users.js`
```javascript
const MongoClient = require('mongodb').MongoClient;
const assert = require('assert');
const bcrypt = require('bcrypt');
const v4 = require('uuid').v4;
const jwt = require('jsonwebtoken');
const jwtSecret = 'SUPERSECRETE20220';

const saltRounds = 10;
const url = 'mongodb://localhost:27017';
const dbName = 'simple-login-db';

const client = new MongoClient(url, {
  useNewUrlParser: true,
  useUnifiedTopology: true,
});

```

`MongoClient` is obviously used for connecting to mongodb and storing the data that the api will be consuming. I like using the `assert` module as a simple validator for the request body and the required data on the endpoints. `bcrypt` is useful for hashing and verifying a password without actually storing it as plain text. [(Please never do that)](https://security.stackexchange.com/questions/120540/why-shouldnt-i-store-passwords-in-plaintext)

The `v4` function is a nice way to create random ids for the users and finally `jwt` is what allows to create a nice session that is secure from the frontend and verified in the backend as well.

I would strongly recommend storing the `jwtSecret` from a `.env` because it is a really bad idea storing it as part of the code commited to github or gitlab since it would be exposed publicly.

Finally you need to setup dbName and a mongo Client for connecting to the db and writing and reading from there.

### Manipulating the DB (to get users and create new ones)

```javascript
function findUser(db, email, callback) {
  const collection = db.collection('user');
  collection.findOne({email}, callback);
}

function createUser(db, email, password, callback) {
  const collection = db.collection('user');
  bcrypt.hash(password, saltRounds, function(err, hash) {
    // Store hash in your password DB.
    collection.insertOne(
      {
        userId: v4(),
        email,
        password: hash,
      },
      function(err, userCreated) {
        assert.equal(err, null);
        callback(userCreated);
      },
    );
  });
}

```

Here's a simple function to `findUser` by email which basically wraps the `collection.findOne()` function and just queries by email and passes the callback.

The `createUser` function is a bit more interesting because first the password needs to be `hashed` and the `insertOne()` happens with the hashed password instead of the plain text version.

The rest of the code which actually will handle the api request, the NextJS as follows:

```javascript
export default (req, res) => {
  if (req.method === 'POST') {
    // signup
    try {
      assert.notEqual(null, req.body.email, 'Email required');
      assert.notEqual(null, req.body.password, 'Password required');
    } catch (bodyError) {
      res.status(403).json({error: true, message: bodyError.message});
    }

    // verify email does not exist already
    client.connect(function(err) {
      assert.equal(null, err);
      console.log('Connected to MongoDB server =>');
      const db = client.db(dbName);
      const email = req.body.email;
      const password = req.body.password;

      findUser(db, email, function(err, user) {
        if (err) {
          res.status(500).json({error: true, message: 'Error finding User'});
          return;
        }
        if (!user) {
          // proceed to Create
          createUser(db, email, password, function(creationResult) {
            if (creationResult.ops.length === 1) {
              const user = creationResult.ops[0];
              const token = jwt.sign(
                {userId: user.userId, email: user.email},
                jwtSecret,
                {
                  expiresIn: 3000, //50 minutes
                },
              );
              res.status(200).json({token});
              return;
            }
          });
        } else {
          // User exists
          res.status(403).json({error: true, message: 'Email exists'});
          return;
        }
      });
    });
  }
};
```

`export default (req, res) => {}` Here's where the magic happens and you get the req, res in a very similar way as you get in an Express app. One of the only things that are required as setup here if you intend to only process the `POST` requests that happen to the endpoint happens here:

`if (req.method === 'POST') { }` 

other HTTP methods could be processed with additional conditions.

The code basically verifies that the body of the request has an email and password otherwise there's not enough info of the user to try to create.

```javascript
    try {
      assert.notEqual(null, req.body.email, 'Email required');
      assert.notEqual(null, req.body.password, 'Password required');
    } catch (bodyError) {
      res.status(403).json({error: true, message: bodyError.message});
    }
```

After basically we verify if a user exists with that email, if it does we throw an error because then it will not make sense to create a second one! Uniqueness should be enforced at least on a field, email is perfect for this.

```javascript
      findUser(db, email, function(err, user) {
        if (err) {
          res.status(500).json({error: true, message: 'Error finding User'});
          return;
        }
```

Finally if no user exists with that email we are safe to go ahead and create it.

```javascript
   createUser(db, email, password, function(creationResult) {
            if (creationResult.ops.length === 1) {
              const user = creationResult.ops[0];
              const token = jwt.sign(
                {userId: user.userId, email: user.email},
                jwtSecret,
                {
                  expiresIn: 3000, //50 minutes
                },
              );
              res.status(200).json({token});
              return;
            }
          });
```

Another relevant thing that is happening here is that the jwt sign is happening. The details of jwt can be found [here](https://jwt.io/) But if all went alright we create a token that contains the userId and email, set it up for some time, 50 minutes in this case and send that as response. 

We'll see how to handle that on the frontend.

## Let's add the `/pages` 🎨

Let's build an `index.js` that displays some content all the time in case visitors don't have a login nor an account. And let's add the logic if the users want to sign up and login for them to see the page a bit different.

Also add the `login.js` and the `signup.js`

### The /pages/signup

The most relevant part of the signup page has to be the submit function that handles the request to the api whenever the user has clicked the submit button. 

```javascript
  function handleSubmit(e) {
    e.preventDefault();
    fetch('/api/users', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        email,
        password,
      }),
    })
      .then((r) => r.json())
      .then((data) => {
        if (data && data.error) {
          setSignupError(data.message);
        }
        if (data && data.token) {
          //set cookie
          cookie.set('token', data.token, {expires: 2});
          Router.push('/');
        }
      });
  }
```

`e.preventDefault()` stops the submission from following the standard procedure and basically redirecting the page.

Then the call to the api happens with the `fetch('/api/users')` call. We send the body as a JSON and here it is important to notice that those values are obtained from hooks set onChange of the inputs.


The most interesting part of this is 
```javascript
        if (data && data.error) {
          setSignupError(data.message);
        }
        if (data && data.token) {
          //set cookie
          cookie.set('token', data.token, {expires: 2});
          Router.push('/');
        }
```

Using the `import cookie from 'js-cookie'` library we set the cookie from the token obtained and set it's expiration for days. This is a discrepancy maybe it is better to set it to 1 day and the JWT for a bit less than that.

Having the cookie set, whenever we make additional requests that cookie is sent to the server as well and there we can decrypt and review if the user is authed properly and that auth is valid.

### POST /auth
This endpoint is very similar to the signup endpoint the main difference and the most interesting part is the Auth method which basically compares the plain text password entered in the body and returns if it matches with the hash stored in the users collection.

```javascript

function authUser(db, email, password, hash, callback) {
  const collection = db.collection('user');
  bcrypt.compare(password, hash, callback);
}
```

Instead of creating the user we just verify if the info entered matches an existing user and return the same jwt token 

```javascript
  if (match) {
              const token = jwt.sign(
                {userId: user.userId, email: user.email},
                jwtSecret,
                {
                  expiresIn: 3000, //50 minutes
                },
              );
              res.status(200).json({token});
              return;
     }
```

### The /pages/login

The login page is basically the same form as the `signup.js` with different texts. Here I would talk a bit more about the hooks used. 

```javascript
const Login = () => {
  const [loginError, setLoginError] = useState('');
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

//...

return (
<input
        name="email"
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
     
)
}

```
Here you can see the basic usage of a react hook. You can store the variable state that you define at the top of your component and set it with the companion function.

Whenever someone changes the email `onChange={(e) => setEmail(e.target.value)}` kicks and sets the value and makes it available through all the component.

[More info on hooks](https://reactjs.org/docs/hooks-intro.html)

### POST /me

```javascript
const jwt = require('jsonwebtoken');
const jwtSecret = 'SUPERSECRETE20220';

export default (req, res) => {
  if (req.method === 'GET') {
    if (!('token' in req.cookies)) {
      res.status(401).json({message: 'Unable to auth'});
      return;
    }
    let decoded;
    const token = req.cookies.token;
    if (token) {
      try {
        decoded = jwt.verify(token, jwtSecret);
      } catch (e) {
        console.error(e);
      }
    }

    if (decoded) {
      res.json(decoded);
      return;
    } else {
      res.status(401).json({message: 'Unable to auth'});
    }
  }
};

```

This endpoint is pretty straightforward yet it is very powerful. Whenever someone makes a `api/me` call the server will look for a `token` key in the `req.cookies` (that is magically managed by [Nextjs middleware](https://nextjs.org/docs/api-routes/api-middlewares)) if said token exists and passes the `jwt.verify` it means the user is validly authed and returns the info decoded (userId and email, remember?) and tells the frontend to keep on, otherwise it returns a 401 `Unauthorized`.

### The /pages/index

Now let's protect a part of the index page to change when you are authed. So it has some difference and you can see the full power of the cookies and the `api/me` endpoint.

![No Login](https://dev-to-uploads.s3.amazonaws.com/i/954c0zvp9xu968bypfzw.png)


What happens for checking the auth:
```javascript
  const {data, revalidate} = useSWR('/api/me', async function(args) {
    const res = await fetch(args);
    return res.json();
  });
  if (!data) return <h1>Loading...</h1>;
  let loggedIn = false;
  if (data.email) {
    loggedIn = true;
  }
 
```

We make a call to the `api/me` endpoint (using the nice lib useSWR, also by zeit team) and if that responds with `data.email` we make the variable `loggedIn` equal to `true` and in the render we can display the email of the user that is logged in and a Log Out button actually! (That simply removes the `token` from the cookies, is that easy!)

```javascript
      {loggedIn && (
        <>
          <p>Welcome {data.email}!</p>
          <button
            onClick={() => {
              cookie.remove('token');
              revalidate();
            }}>
            Logout
          </button>
        </>
      )}
      {!loggedIn && (
        <>
          <Link href="/login">Login</Link>
          <p>or</p>
          <Link href="/signup">Sign Up</Link>
        </>
      )}
 
```

![Logged](https://dev-to-uploads.s3.amazonaws.com/i/nsevabtol8jwvuze356i.png)

Full code for the page component:

```javascript
import Head from 'next/head';
import fetch from 'isomorphic-unfetch';
import useSWR from 'swr';
import Link from 'next/link';
import cookie from 'js-cookie';

function Home() {
  const {data, revalidate} = useSWR('/api/me', async function(args) {
    const res = await fetch(args);
    return res.json();
  });
  if (!data) return <h1>Loading...</h1>;
  let loggedIn = false;
  if (data.email) {
    loggedIn = true;
  }
  return (
    <div>
      <Head>
        <title>Welcome to landing page</title>
        <meta name="viewport" content="initial-scale=1.0, width=device-width" />
      </Head>
      <h1>Simplest login</h1>

      <h2>Proudly using Next.js, Mongodb and deployed with Now</h2>
      {loggedIn && (
        <>
          <p>Welcome {data.email}!</p>
          <button
            onClick={() => {
              cookie.remove('token');
              revalidate();
            }}>
            Logout
          </button>
        </>
      )}
      {!loggedIn && (
        <>
          <Link href="/login">Login</Link>
          <p>or</p>
          <Link href="/signup">Sign Up</Link>
        </>
      )}
    </div>
  );
}

export default Home;

```

Remember the whole code is available here: 
[https://github.com/mgranados/simple-login](https://github.com/mgranados/simple-login) for your review!

That's it! Thanks getting this far! Hope you got a good hold of what it is like to build an api and pages with Next.JS and I hope you are motivated to build your own stuff. 

If you liked or have doubts and I could help you with something JS related please ping me on Twitter! [@martingranadosg] (https://twitter.com/martingranadosg) I would love to know what you can build with this! :) or ping me here in `dev.to` as well 😁