User Authentication in MERN Stack Application.

--

mern stack
credit : wikimedia.org

I’ve seen plenty of people struggle when it comes to authentication in MERN stack, though it’s fairly easy. In this tutorial, we will go through the whole process of initialising our react app and server to successfully registering our user with encrypted password to then logging in to fetch their information from our No-SQL database - MongoDB.

Prerequisites -

Let’s begin by initialising our backend directory by npm init, which will create the package.json file and allow us to install necessary packages for our backend.

Now, initialise our frontend by running the following command…

npx create-react-app frontend

You may name your app anything, in our case we will call it frontend.

In our root directory, install the following packages -

npm install express express-async-handler jsonwebtoken mongoose bcryptjs

express - It is a minimal and flexible Node.js web application framework that provides a robust set of features for web and mobile applications.

express-async-handler - A simple middleware for handling exceptions inside of async express routes and passing them to your express error handlers.

jsonwebtoken - It is an open standard that defines a compact and self-contained way for securely transmitting information between parties as a JSON object.

mongoose - It is an Object Data Modelling library for MongoDB and Node.js. It manages relationships between data, provides schema validation,etc.

bcryptjs - It is a library providing secured way to store passwords in database regardless of whatever language app’s backend is built in.

Creating the server (server.js)

  • Import the express package and assign it to a variable.
  • Now pass PORT number and a connection message to app.listen() as a parameter.
const express = require("express");const app = express();PORT = 5000;
app.listen(PORT, console.log(`Server running on PORT ${PORT}..`));

Now run node backend/server in the terminal, and boom you’ve created your server successfully.

Lets create our first route to test out our server, add this bold code in server.js

const express = require("express");const app = express();// GET Request to "/" route
app.get("/", (req, res) => {
res.send("API is running..");
});
PORT = 5000;
app.listen(PORT, console.log(`Server running on PORT ${PORT}..`));

Now open localhost:5000 in your browser and you should see the “API is running” message.

Note - You need to restart your server every time you make changes to your backend code. To avoid that, you can install a package in root directory called nodemon and run “nodemon backend/server” in terminal.

Connecting to MongoDB database

  • Login to your MongoDB Atlas account and create a new Project.
  • Create a new Cluster with any name (“tutorials” in our case).
  • Click on connect > connect your application > select node.js from driver drop-down and copy your mongo URI.
  • Create a new user in “Database Access” tab and click “add new database user”.
  • In “Network Access” tab, create a new IP, either yours or if you wanna use it as Rest API, you may enter “0.0.0.0/0”.
  • Head back to our backend folder, create a new file db.js
  • Replace the <dbname> and <password> with your database access credentials created earlier.
const mongoose = require("mongoose");const connectDB = async () => {MONGO_URI ="mongodb+srv://roadsidecoder:<password>@tutorials.pieze.mongodb.net/<dbname>?retryWrites=true&w=majority";try {
const conn = await mongoose.connect(MONGO_URI, {
useUnifiedTopology: true,
useNewUrlParser: true,
useCreateIndex: true,
});
console.log(`MongoDB Connected: ${conn.connection.host}`);} catch (error) {
console.error(`Error: ${error.message}`);
process.exit();
}
};
module.exports = connectDB;
  • Import the connectDB() funtion above “const app = express()” in your server.js and run it again. If you see this, your db is connected.
mongo db connected

Alright, Now that we’re done setting up our backend, lets finally create authentication model and routes.

Creating the User Schema (userModel.js)

A document schema is a JSON object that allows you to define the shape and content of documents and follow the same JSON schema specification as document validation in the MongoDB server.

  • Create a new file in backend folder - userModel.js
  • Our User Schema will have Name, Email and password fields along with the timestamp which will be created automatically when the document is saved in database.
const mongoose = require("mongoose");const userSchema = mongoose.Schema(
{
name: {
type: String,
required: true,
},
email: {
type: String,
required: true,
unique: true,
},
password: {
type: String,
required: true,
},
},
{
timestamps: true,
}
);
const User = mongoose.model("User", userSchema);module.exports = User;
  • Create a pre save middleware function which will encrypt our password field before saving it to our database.
  • We will use bcryptjs library for that. Read more about bcryptjs here.
const mongoose = require("mongoose");
import bcrypt from "bcryptjs";
const userSchema = mongoose.Schema(
{
name: {
type: String,
required: true,
},
email: {
type: String,
required: true,
unique: true,
},
password: {
type: String,
required: true,
},
},
{
timestamps: true,
}
);
// will encrypt password everytime its saved
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;
  • Similarly to compare the encrypted password while logging in, we will add a matchPassword function
const mongoose = require("mongoose");
import bcrypt from "bcryptjs";
const userSchema = mongoose.Schema(
{
name: {
type: String,
required: true,
},
email: {
type: String,
required: true,
unique: true,
},
password: {
type: String,
required: true,
},
},
{
timestamps: true,
}
);
userSchema.methods.matchPassword = async function (enteredPassword)
{
return await bcrypt.compare(enteredPassword, this.password);
};
// will encrypt password everytime its saved
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;

Defining User Route (server.js)

To create user routes, we will create a separate file so that server.js doesn’t get too crowded with all the code.

  • create userRoutes.js in backend folder
  • Add “/api/user ”route in server .js and import userRoutes file in it.
const express = require("express");
const connectDB = require("./db");
const userRoutes = require("./userRoutes");
connectDB();const app = express();app.use(express.json()); // To accept JSON data from api requestsapp.get("/", (req, res) => {
res.send("API is running..");
});
app.use("/api/user", userRoutes);PORT = 5000;
app.listen(PORT, console.log(`Server running on PORT ${PORT}..`));

Creating Registration route (userRoutes.js)

  • Import express , express-async-handler and user model.
const express = require("express");
const asyncHandler = require("express-async-handler");
const User = require("./userModel");
  • To create routes we will need a function from express called Router()
const router = express.Router();
  • Create a post request to “/register” and wrap the async function in asyncHandler to handle errors.
const express = require("express");
const asyncHandler = require("express-async-handler");
const User = require("./userModel");
const router = express.Router();
router.post("/register", asyncHandler(async (req, res) => {}));module.exports = router;
  • Then we will accept name,email and password from the body data
  • Check if the email already exists in our database.
const express = require("express");
const asyncHandler = require("express-async-handler");
const User = require("./userModel");
const router = express.Router();
router.post("/register", asyncHandler(async (req, res) => {const { name, email, password } = req.body;const userExists = await User.findOne({ email });

}));
module.exports = router;
  • If user already exists, throw an error 404.
const express = require("express");
const asyncHandler = require("express-async-handler");
const User = require("./userModel");
const router = express.Router();
router.post("/register", asyncHandler(async (req, res) => {const { name, email, password } = req.body;
const userExists = await User.findOne({ email });
if (userExists) {
res.status(404);
throw new Error("User Already Exists");
}
}));module.exports = router;
  • If user doesn’t exists, create a new document in the database
const express = require("express");
const asyncHandler = require("express-async-handler");
const User = require("./userModel");
const router = express.Router();
router.post("/register", asyncHandler(async (req, res) => {const { name, email, password } = req.body;
const userExists = await User.findOne({ email });
if (userExists) {
res.status(404);
throw new Error("User Already Exists");
}
const user = await User.create({name,email,password,});}));module.exports = router;
  • If user is saved successfully respond with user details else with error.
const express = require("express");
const asyncHandler = require("express-async-handler");
const User = require("./userModel");
const router = express.Router();
router.post("/register", asyncHandler(async (req, res) => {const { name, email, password } = req.body;
const userExists = await User.findOne({ email });
if (userExists) {
res.status(404);
throw new Error("User Already Exists");
}
const user = await User.create({name,email,password,});if (user) {
res.status(201).json({
_id: user._id,
name: user.name,
email: user.email,
});
}
else {
res.status(400);
throw new Error("User not found");
}
}));module.exports = router;

Creating Login route (userRoutes.js)

  • Create a post request to “/login” and wrap the async function in asyncHandler to handle errors.
router.post("/login", asyncHandler(async (req, res) => {}));
  • Then we will accept email and password from the body data
  • Check if the email already exists in our database.
router.post("/login", asyncHandler(async (req, res) => {const { email, password } = req.body;
const user = await User.findOne({ email });
}));
  • If user exists and the “matchPassword” returns true, we will respond with user info else an error
router.post("/login", asyncHandler(async (req, res) => {const { email, password } = req.body;
const user = await User.findOne({ email });
if (user && (await user.matchPassword(password))) {
res.json({
_id: user._id,
name: user.name,
email: user.email,
});
} else {
res.status(401);
throw new Error("Invalid Email or Password");
}
}));

But we need to return a JWT token as well to frontend : —

  • We will create a function to generate JWT Token in a new file “generateToken.js”.
const jwt = require("jsonwebtoken");const generateToken = (id) => {const JWT_SECRET = "roadsidecoder123"; 
//Give some value to your JWT_SECRET.
return jwt.sign({ id }, , {
expiresIn: "30d",
});
};
module.exports = generateToken;
  • import this function to our login route
const generateToken = require("./generateToken");router.post("/login", asyncHandler(async (req, res) => {const { email, password } = req.body;
const user = await User.findOne({ email });
if (user && (await user.matchPassword(password))) {
res.json({
_id: user._id,
name: user.name,
email: user.email,
token: generateToken(user._id),
});
} else {
res.status(401);
throw new Error("Invalid Email or Password");
}
}));

And that’s it !! We have successfully created our API routes for Login and Registration.

Now lets move on to our frontend and create user interface to test our login and registration API.

Leave our backend server running, we’ll get back to it soon. Switch to frontend folder in another terminal.

Assuming you have the basic understanding of react, you can create three pages each for login , registration and profile.

Find the full code for frontend here -

Registration Page Functioning-

After creating the registration form,

  • Create a the state for name, email and password and then you need to provide the following function in onSubmit function.
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const handleSubmit = async (e) => {
e.preventDefault();
const config = {
headers: {
"Content-type": "application/json",
},
};
// POST Request to register API
const { data } = await axios.post(
"/api/user/register",
{ name, email, password },
config
);
localStorage.setItem("user", JSON.stringify(data));
setUserInfo(JSON.parse(localStorage.getItem("user")));
};

Login Page Functioning-

After creating the login form,

  • Create a the state for email and password and then you need to provide the following function in onSubmit function.
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const handleSubmit = async (e) => {
e.preventDefault();
const config = {
headers: {
"Content-type": "application/json",
},
};
// POST Request to register API
const { data } = await axios.post(
"/api/user/login",
{ email, password },
config
);
localStorage.setItem("user", JSON.stringify(data));
setUserInfo(JSON.parse(localStorage.getItem("user")));
};

Since, we are storing the user data in localstorage, you can fetch it from local storage to use it in any of the components.

To logout, simple clear the user data from the localstorage as follows -

localStorage.clear();

Find the full project’s source code here -

Thank you for Reading. Peace !

I’d like to hear from you - @RoadsideCoder on Instagram.

--

--