JWT User Authentification in Node.js using Express.js and MongoDB
Tags: node, javascript, webdev, mongodb
Published: 2023-03-16 06:30:30

To get started with this project, it is recommended to set up a basic project with separate folders for the client and server. This will help maintain a clear separation of concerns between frontend and backend functionalities.

**MongoDB setup**

To create a MongoDB database, you need to create an account, and then click on "Build Database." The free version is sufficient for our project. Choose the cloud, I am sticking to the default AWS.

Next, it will prompt you to create a username and password for your database. Pay attention to password you use as we will need it when setting up mongoDB in the project. For this project, I chose the cloud environment, and set the IP address to 0.0.0.0 (Note: This is not secure and is only used for this example project. For your real project, use a specific IP or range of IPs).

Once you have created your database, click "Connect" and choose "Connect to Application" and select Node.js as a driver. Copy the provided string, we will need in few moments for our project.

**Setting up the server and database connection**

Now we need to set up our environment variables by configuring the .env file. In the .env file, we'll add the MongoDB connection URL that we copied earlier, along with other variables such as the port number and token secret. Make sure to replace the password in the MongoDB connection URL with the actual password that you created earlier for your database.

```javascript
PORT=5001
NODE_ENV=development
MONGO_URL= mongodb+srv://kristenking0110:dev321@cluster0.f9lb2mc.mongodb.net/?retryWrites=true&w=majority
TOKEN_SECRET = my_secret

```
Next, we create a server.js file where we use Express.js to create our server and connect to the MongoDB database using the connectDB() function. We then define our routes and start listening to incoming requests on the specified port number.

```javascript
const dotenv = require('dotenv');
dotenv.config({ path: './config/config.env' });
const express = require('express');
const connectDB = require('./config/database');
const logger = require('./utils/logger');
const userRouter = require('./routes/userRouter');

const app = express();

// Connect to database
connectDB();

app.use(express.json());

const PORT = process.env.PORT || 5001;

// Use routes
app.use('/users', userRouter);

const server = app.listen(PORT, () => {
  console.log(`Server is listening on PORT ${PORT}`);
});

process.on('unhandledRejection', (err) => {
  console.log(`Error: ${err.message}`);
  server.close(() => process.exit(1));
});
```

We also need to create a database.js file, where we use Mongoose to connect to the MongoDB database and log a message indicating a successful connection.


```javascript
const mongoose = require('mongoose'); 

const connectDB = async () => {
    const conn = await mongoose.connect(process.env.MONGO_URL, {
        useNewUrlParser: true, 
        useUnifiedTopology: true
    })

    console.log(`MongoDB Connected: ${conn.connection.host}`)
}

module.exports = connectDB;
```

**Installing packages**

Make sure that you are only working within your server folder. Run 'npm init' to generate a separate package.json file. Make sure that your folder structure is set up appropriately.


![folder structure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z5l3fktxrg006j65r8fy.png)

Now, let's install all the packages we'll use for this project:

```javascript
npm i express mongoose dotenv jsonwebtoken bcrypt express-handler
```

Before proceeding, you can run your server to check if the MongoDB connection is successful and ready to go. 

![mongoDB connection](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/112snhuk8ylffak1wzo9.png)

**Create userSchema**

Once you have installed the necessary packages and ensured that your MongoDB connection is successful, the next step is to create the userSchema. This schema defines the structure of the user data that will be stored in the database. Additionally, we include a matchPasswords method to validate the entered password, as well as a pre-save hook to hash the password before storing it in the database.

```javascript
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const bcrypt = require('bcryptjs');

const userSchema = new Schema(
  {
    firstName: {
      type: String,
      required: [true, "Please provide a first name"],
    },
    lastName: {
      type: String,
      required: [true, "Please provide a last name"],
    },
    email: {
      type: String,
      unique: true,
      required: [true, "Please provide an email address"],
    },
    password: {
      type: String,
      required: [true, "Please provide a password"],
    },
    profileImage: {
      type: String,
    },
    isAdmin: {
      type: Boolean,
      default: false,
    }
  },
  { timestamps: true }
);

userSchema.methods.matchPasswords = function (enteredPassword) {
  return bcrypt.compare(enteredPassword, this.password);
};

userSchema.pre('save', async function (next) {
  if (!this.isModified('password')) {
    next();
  }

  const salt = await bcrypt.genSalt(10);
  this.password = await bcrypt.hash(this.password, salt)
});

const User = mongoose.model('User', userSchema);
module.exports = User;
```

**Create userController**

Next we need to create userController that will handle all user-related requests such as login, registration and I included some CRUD opertions.

```javascript
const express = require('express');
const User = require('../models/users');
const asyncHandler = require("express-async-handler");
const jwt = require("jsonwebtoken");



const genToken = (id) => {
  return jwt.sign({ id }, process.env.TOKEN_SECRET, { expiresIn: '60d' });
}

const loginUser = asyncHandler(async (req, res) => {
  const { email, password } = req.body;
  const user = await User.findOne({ email });

  if (user && (user.matchPasswords(password))) {
    res.json({
      _id: user._id,
      firstName: user.firstName,
      lastName: user.lastName,
      isAdmin: user.isAdmin,
      token: genToken(user._id)
    })
  } else {
    res.status(401);
    throw new Error('Invalid email or password.')
  }
}
);


const registerUser = asyncHandler(async (req, res) => {
  const { firstName, lastName, email, password } = req.body;

  const userExists = await User.findOne({ email });
  if (userExists) {
    res.status(400)
    throw new Error("We already have an account with that email address. ");
  }

  const user = await User.create({
    firstName,
    lastName,
    email,
    password,
  });

  if (user) {
    res.status(201).json({
      _id: user._id,
      firstName: user.firstName,
      lastName: user.lastName,
      email: user.email,
      isAdmin: user.isAdmin,
      token: genToken(user._id),
    });
  } else {
    res.status(400);
    throw new Error('Invalid user data');
  }
});

const getUsers = async (req, res, next) => {
  try {
    const result = await User.find()

    res
      .status(200)
      .setHeader('Content-Type', 'application/json')
      .json(result)
  } catch (err) {
    throw new Error(`Error retrieving users: ${err.message}`)
  }
}

const postUser = async (req, res, next) => {
  try {
    const result = await User.create(req.body)

    res
      .status(201)
      .setHeader('Content-Type', 'application/json')
      .json(result)
  } catch (err) {
    throw new Error(`Error displaying a new user: ${err.message}`)
  }
}

const deleteUsers = asyncHandler(async (req, res, next) => {
  try {
    await User.deleteMany({});
    res.status(200).json({
      success: true,
      msg: 'All users were deleted'
    });
  } catch (err) {
    throw new Error(`Error deleting users: ${err.message}`);
  }
});

const getUser = asyncHandler(async (req, res, next) => {
  try {
    const result = await User.findById(req.params.id)
    if (!result) {
      res.status(404)
      throw new Error("User not found.");
    }

    res
      .status(200)
      .setHeader('Content-Type', 'application/json')
      .json(result)
  } catch (err) {
    throw new Error(`Error retrieving user: ${err.message}`)
  }
})

const putUser = asyncHandler(async (req, res, next) => {
  try {
    const result = await User.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true });
    if (!result) {
      res.status(404)
      throw new Error("User not found.");
    }

    res
      .status(200)
      .setHeader('Content-Type', 'application/json')
      .json(result)
  } catch (err) {
    throw new Error(`Error updating user: ${err.message}`)
  }
})

const deleteUser = asyncHandler(async (req, res, next) => {
  const userId = req.params.id;

  const user = await User.findById(userId);
  if (!user) {
    res.status(404);
    throw new Error('User not found');
  }

  await user.remove();

  res.json({ success: true, message: 'User deleted successfully' });
});


module.exports = {
  getUsers,
  postUser,
  deleteUsers,
  getUser,
  putUser,
  deleteUser,
  loginUser,
  registerUser
}
```

**Create useRouter**

Once the userSchema and userController are set up, we can define the routes that will handle user-related requests. 

```javascript
const express = require('express');
const userController = require('../controllers/userController');

const userRouter = express.Router();

userRouter.get('/', userController.getUsers);
userRouter.post('/', userController.postUser);
userRouter.delete('/', userController.deleteUsers);
userRouter.get('/:id', userController.getUser);
userRouter.put('/:id', userController.putUser);
userRouter.delete('/:id', userController.deleteUser);
userRouter.post('/login', userController.loginUser);
userRouter.post('/register', userController.registerUser);

module.exports = userRouter;
```

**Check on Postman**

Now we can check the registration and login user in Postman.


![register user](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9cf4pydpktph03qq9897.png)


![login user](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fglqvfxybwjeob3f372i.png)

I hope this article helped to provide a general understanding of user authentification concepts.