MERN Stack User Auth API

The less opinionated nature of the MERN stack makes it very easy to learn for beginners. This also opens the door to a very messy file structure and poor project management. In this article, we will learn about how to organize your MERN App for an API project. This can also be applied to other different types of projects.

We are going to build a real-time user auth/sign-in and sign-up/register app. After that, we will create a middleware for authorizing users to different pages.

For this tutorial, I assume you have a basic understanding of Node.js, Express.js, MongoDB, and ReactJS.

Environment Setup

I prefer the below-mentioned tools for my local environment, you can choose to have a different setup. If you don’t have any preferences, I would encourage you to follow the below-mentioned setup for your environment. 

Make sure to install the stable versions.

Initiate the project by creating a repository and installing dependencies.

Create a new repository in your preferred location and open it with Visual Studio code. Once you open the repository in VS code, from the top navbar open the terminal or use the keyboard shortcut ctrl + `. Go to the terminal and initiate npm with the command npm init -y.

Note: -y is used for ignoring package information. If you want to add package info you can choose to remove -y from the above command. 

MERN Stack User Auth API 1

Your project should look like this.

Installing dependencies

Run the following command in the same directory to install dependencies.

npm install express mongoose bcrypt jsonwebtoken express-validator dotenv cors

Note: based on your internet speed this is going to take some time.

Once the above installation is done we will install one last package which will help us in our development process. We are only going to install it for the development environment.

npm install -D nodemon

While the nodemon is installing, let’s create a new file inside the root directory and name it app.js. After that, set up your base scripts in package.json file.

package.json - Before

				
					{
  "name": "user-auth-app",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "bcrypt": "^5.0.1",
    "cors": "^2.8.5",
    "express": "^4.18.1",
    "jsonwebtoken": "^8.5.1",
    "mongoose": "^6.4.6",
    "validator": "^13.7.0"
  },
  "devDependencies": {
    "nodemon": "^2.0.19"
  }
}
				
			

package.json - After

				
					{
  "name": "user-auth-app",
  "version": "1.0.0",
  "description": "",
  "main": "app.js",
  "scripts": {
    "start": "node app.js",
    "dev": "nodemon app.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "bcrypt": "^5.0.1",
    "cors": "^2.8.5",
    "express": "^4.18.1",
    "jsonwebtoken": "^8.5.1",
    "mongoose": "^6.4.6",
    "express-validator": "^13.7.0"
  },
  "devDependencies": {
    "nodemon": "^2.0.19"
  }
}

				
			

I have changed scripts for the development server and for the production server. Also, you will see the “main” above scripts. I have changed that to app.js based on our server starting file. Some people call it server.js or index.js by default. You are welcome to name it anything you like but keep it relevant. 

Your project should look like below screenshot at this point.

MERN App basic setup screenshot

Smoke Test

app.js

				
					const express = require("express");
const app = express();
const port = 3000;

app.get("/", (req, res) => {
  res.send("Hello World!");
});

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`);
});

				
			

Now we will open the app.js file and add the above code and run the file with npm run dev

Open http://localhost:3000/

You will see “Hello world!” printed on your screen. 

Recap

🥳 Yayyyy 🥳

  • You have set up your local environment.
  • You have set up your express app.
  • You have installed the development dependencies as well. 

If you have come so far, I want you to tap yourself on the back. That’s really an impressive milestone you have covered so far.

Let’s understand what we have done so far. 

Installed packages so far

Folder structure setup

We will be creating some files and folders which I will explain at the end. Follow me carefully for the next few steps and we will make our project directory look more organized. This is important for so many reasons, one being to help you split your code into similar files and folders. If anyone else looks into your project they should know where to find things. 

				
					# in your root directory add config
mkdir config

# create repo for client
mkdir client

# creating folder structure for API and versions
mkdir api
cd api
mkdir v1
cd v1
mkdir controllers, routers, routers, middlewares
				
			

Create config > default.json

Create config > db.js

Create api > index.js

Create api > v1 > controllers > user > auth.js

Create api > v1 > controllers > user > user.js

Create api > v1 > middlewares > auth.js

Create api > v1 > routers > profile.js

Create api > v1 > routers > user.js

MERN Stack User Auth API 2

This is how your folder structure should look at this stage.

I have already initiated git on my repository to upload the code to my GitHub account. Please ignore the .env and .gitignore files for now. 

app.js

				
					const express = require("express");
const connectDB = require("./config/db");

// importing routes
const api = require("./api");

const app = express();

// init db connection
connectDB();

// API users
app.use("/api", api);

// Starting app
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`App is started on: http://localhost:${PORT}`);
});

				
			

config > default.json

				
					{
    "mongoURI": "mongodb://localhost:27017/userauth",
    "jwtSecret": "mySecret"
}
				
			

config > db.js

				
					const mongoose = require("mongoose");
const config = require("config");
const db = config.get("mongoURI");

const connectDB = async () => {
  try {
    // connecting mongodb
    await mongoose.connect(db);
    console.log("MongoDB Connected!");
  } catch (err) {
    console.log(err);

    // Exit process with failure
    process.exit(1);
  }
};

module.exports = connectDB;

				
			

api > index.js

				
					const express = require("express");
const router = express.Router();
const user = require("./v1/routers/user");
const profile = require("./v1/routers/profile");

router.use(express.json({ extended: true }));

router.get("/", (req, res) => {
  res.send("something");
});

router.use("/v1/user", user);
router.use("/v1/profile", profile);

module.exports = router;

				
			

api > v1 > routers > user.js

				
					const express = require("express");
const router = express.Router();

//importing user controllers
const userCtrl = require("../controllers/user/user");
const authCtrl = require("../controllers/user/auth");

router.get("/", (req, res) => res.send("user get - nested router"));

router.post("/", userCtrl);

router.post("/auth", authCtrl);

module.exports = router;

				
			

api > v1 > routers > profile.js

				
					const express = require("express");
const router = express.Router();
const Auth = require("../middlewares/auth");

router.get("/", Auth, (req, res) => {
  res.json({ msg: "Authorized" });
});

module.exports = router;

				
			

api > v1 > controllers > user > user.js

				
					const User = require("../../models/User");
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
const config = require("config");

const saltRounds = 10;

module.exports = userCtrl = async (req, res) => {
  try {
    const { name, email, password } = req.body;
    // check if user already exists
    let user = await User.findOne({ email: req.body.email });

    // return response if user already exists
    if (user) {
      return res
        .status(401)
        .json({ msg: "User already exists!", type: "Danger" });
    }

    user = {
      name: name,
      email: email,
      password: bcrypt.hashSync(password, saltRounds),
    };

    user = await User.create(user);
    delete user.password;

    const token = await jwt.sign(
      {
        exp: Math.floor(Date.now() / 1000) + 60 * 60 * 48,
        data: user,
      },
      config.get("jwtSecret")
    );

    res.json({ token });
  } catch (err) {
    console.log(err.message);
    res.status(500).json({ msg: "Server error!" });
  }
};

				
			

api > v1 > controllers > user > auth.js

				
					const User = require("../../models/User");
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
const config = require("config");

const saltRounds = 10;

module.exports = authCtrl = async (req, res) => {
  try {
    const { email, password } = req.body;
    let user = await User.findOne({ email });
    const isPwdCorrect =
      user && (await bcrypt.compare(password, user?.password));

    // checking input details with db records
    if (!user || !isPwdCorrect)
      return res.status(404).json({ msg: "Wrong email or password!" });

    const token = await jwt.sign(
      {
        exp: Math.floor(Date.now() / 1000) + 60 * 60 * 48,
        data: user,
      },
      config.get("jwtSecret")
    );

    res.json({ token });
  } catch (err) {
    console.log(err.message);
    res.status(500).json({ msg: "server error!" });
  }
};

				
			

api > v1 > middlewares > auth.js

				
					const jwt = require("jsonwebtoken");
const config = require("config");
 

module.exports = Auth = (req, res, next) => {
  const token = req.header("auth-token");

  // check if no token
  if (!token)
    return res.status(401).json({ msg: "No token, Authorization denied!" });

  try {
    const decoded = jwt.verify(token, config.get("jwtSecret"));
    req.user = decoded.user;
    next();
  } catch (err) {
    console.log(err.message);
    if (err.message === "invalid token")
      return res.status(401).json({ msg: err.message });
    return res.status(401).json({ msg: "server error!" });
  }
};

				
			

api > v1 > models > User.js

				
					const mongoose = require("mongoose");
const { Schema } = mongoose;

const userSchema = new Schema({
  name: String,
  email: String,
  password: String,
  date: { type: Date, default: Date.now },
});

const User = mongoose.model("User", userSchema);

module.exports = User;

				
			

Let’s test the project with Postman. You can go to their learn page.

Run the project with npm run dev

If you see any errors in the console/terminal. Comment below with the error message and I will try my best to get back to you. Try to answer any questions in the comments section if you think you can help someone. 

We have come a long way and you should look further into the documentation of each package and library. You can not be a great developer without understanding the technology from their documentation. 

Best of luck! 

People also look into these topics.

Leave a comment

Leave a Reply

Your email address will not be published.