Mastering Authentication in MERN Stack Apps with JWT
Mastering Authentication in MERN Stack Apps with JWT In this tutorial, we will focus on implementing JWT Authentication in a MERN stack application. JWT is a popular and efficient way to handle user authentication in modern web apps. By the end of this tutorial, you will be able to set up user registration, login, and secure routes using JWT. What is JWT? JWT (JSON Web Token) is an open standard (RFC 7519) that defines a compact and self-contained way to securely transmit information between parties as a JSON object. This token is widely used for authentication and information exchange in web applications. Benefits of Using JWT Stateless Authentication: JWT is stateless, meaning you don’t need to store session information on the server. Secure: JWT tokens can be signed and optionally encrypted, providing security and integrity of data. Scalable: Since JWT is stateless, it is ideal for scaling applications across multiple servers. How JWT Works Client Logs In: The user provides their credentials (username and password) to the server. Server Validates Credentials: The server validates the credentials against the database. JWT Creation: If the credentials are correct, the server generates a JWT token that contains user information (typically, user ID and other claims). Client Receives Token: The client stores the token (usually in local storage or cookies) and includes it in the Authorization header of future requests. Token Validation: On every API call, the server validates the token to check if the user is authenticated. Project Setup To get started, let's create a basic MERN stack application with MongoDB, Express.js, React.js, and Node.js. This will include user registration and login functionality. First, initialize the Node.js app: mkdir mern-jwt-auth cd mern-jwt-auth npm init -y Install the required dependencies: npm install express mongoose jsonwebtoken bcryptjs dotenv cors npm install --save-dev nodemon Set up your backend structure like so: backend/ ├── config/ ├── controllers/ ├── middleware/ ├── models/ ├── routes/ └── server.js Creating the User Model Let’s break down the User model creation and why we define things the way we do: In the models folder, create a User.js file with the following code to define the User schema: const mongoose = require('mongoose'); const bcrypt = require('bcryptjs'); const userSchema = new mongoose.Schema({ name: { type: String, required: true, }, email: { type: String, required: true, unique: true, }, password: { type: String, required: true, }, }); // Hash the password before saving to the database userSchema.pre('save', async function (next) { if (!this.isModified('password')) return next(); this.password = await bcrypt.hash(this.password, 10); next(); }); const User = mongoose.model('User', userSchema); module.exports = User; Explaining the Model Structure User Schema: We define the User schema using Mongoose, which allows us to model our MongoDB data. The schema defines the structure of the documents we store in the database. For instance, the name, email, and password fields are crucial for our authentication process. Email as Unique: We ensure the email field is unique so that no two users can register with the same email address. This is important for preventing conflicts during the authentication process and ensures each user is identified by a unique email. Hashing the Password: We use bcryptjs to hash the user's password before saving it to the database. This is done in the pre('save') hook, which is executed before the user document is saved to the database. Why Hash? Storing passwords as plain text is highly insecure. Hashing ensures that even if the database is compromised, the actual passwords remain unreadable. Why this.isModified('password')?: This check ensures that we only hash the password when it is newly created or updated, avoiding unnecessary hashing if the password hasn't changed. Register and Login Routes Now, let’s set up the registration and login routes. These routes will handle user registration and authentication. In the routes folder, create a authRoutes.js file: const express = require('express'); const bcrypt = require('bcryptjs'); const jwt = require('jsonwebtoken'); const User = require('../models/User'); const router = express.Router(); // Register user router.post('/register', async (req, res) => { const { name, email, password } = req.body; const userExists = await User.findOne({ email }); if (userExists) return res.status(400).json({ msg: 'User already exists' }); const user = new User({ name, email, password }); await user.save(); res.json({ msg: 'User registered successfully' }); }); // Login user router.post('/login', async (req, res) => { const { email, password } = req.body; const user = await User

Mastering Authentication in MERN Stack Apps with JWT
In this tutorial, we will focus on implementing JWT Authentication in a MERN stack application. JWT is a popular and efficient way to handle user authentication in modern web apps. By the end of this tutorial, you will be able to set up user registration, login, and secure routes using JWT.
What is JWT?
JWT (JSON Web Token) is an open standard (RFC 7519) that defines a compact and self-contained way to securely transmit information between parties as a JSON object. This token is widely used for authentication and information exchange in web applications.
Benefits of Using JWT
- Stateless Authentication: JWT is stateless, meaning you don’t need to store session information on the server.
- Secure: JWT tokens can be signed and optionally encrypted, providing security and integrity of data.
- Scalable: Since JWT is stateless, it is ideal for scaling applications across multiple servers.
How JWT Works
- Client Logs In: The user provides their credentials (username and password) to the server.
- Server Validates Credentials: The server validates the credentials against the database.
- JWT Creation: If the credentials are correct, the server generates a JWT token that contains user information (typically, user ID and other claims).
- Client Receives Token: The client stores the token (usually in local storage or cookies) and includes it in the Authorization header of future requests.
- Token Validation: On every API call, the server validates the token to check if the user is authenticated.
Project Setup
To get started, let's create a basic MERN stack application with MongoDB, Express.js, React.js, and Node.js. This will include user registration and login functionality.
First, initialize the Node.js app:
mkdir mern-jwt-auth
cd mern-jwt-auth
npm init -y
Install the required dependencies:
npm install express mongoose jsonwebtoken bcryptjs dotenv cors
npm install --save-dev nodemon
Set up your backend structure like so:
backend/
├── config/
├── controllers/
├── middleware/
├── models/
├── routes/
└── server.js
Creating the User Model
Let’s break down the User model creation and why we define things the way we do:
In the models
folder, create a User.js
file with the following code to define the User schema:
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const userSchema = new mongoose.Schema({
name: {
type: String,
required: true,
},
email: {
type: String,
required: true,
unique: true,
},
password: {
type: String,
required: true,
},
});
// Hash the password before saving to the database
userSchema.pre('save', async function (next) {
if (!this.isModified('password')) return next();
this.password = await bcrypt.hash(this.password, 10);
next();
});
const User = mongoose.model('User', userSchema);
module.exports = User;
Explaining the Model Structure
User Schema: We define the User schema using Mongoose, which allows us to model our MongoDB data. The schema defines the structure of the documents we store in the database. For instance, the
name
,email
, andpassword
fields are crucial for our authentication process.Email as Unique: We ensure the
email
field is unique so that no two users can register with the same email address. This is important for preventing conflicts during the authentication process and ensures each user is identified by a unique email.Hashing the Password: We use bcryptjs to hash the user's password before saving it to the database. This is done in the
pre('save')
hook, which is executed before the user document is saved to the database. Why Hash? Storing passwords as plain text is highly insecure. Hashing ensures that even if the database is compromised, the actual passwords remain unreadable.Why
this.isModified('password')
?: This check ensures that we only hash the password when it is newly created or updated, avoiding unnecessary hashing if the password hasn't changed.
Register and Login Routes
Now, let’s set up the registration and login routes. These routes will handle user registration and authentication. In the routes
folder, create a authRoutes.js
file:
const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const router = express.Router();
// Register user
router.post('/register', async (req, res) => {
const { name, email, password } = req.body;
const userExists = await User.findOne({ email });
if (userExists) return res.status(400).json({ msg: 'User already exists' });
const user = new User({ name, email, password });
await user.save();
res.json({ msg: 'User registered successfully' });
});
// Login user
router.post('/login', async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user) return res.status(400).json({ msg: 'User does not exist' });
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) return res.status(400).json({ msg: 'Invalid credentials' });
const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET, { expiresIn: '1h' });
res.json({ token });
});
module.exports = router;
Why Middleware is Important
In the above code, we use middleware to handle various authentication processes:
Register Route: First, we check if the user already exists using
User.findOne({ email })
. This prevents multiple users from registering with the same email address. If the email already exists, we return an error message.Login Route: After validating the email, we use bcryptjs to compare the provided password with the hashed password stored in the database. If the password is incorrect, we return an error.
JWT Generation: Once the user’s credentials are validated, we create a JWT token using
jwt.sign()
. Theid
of the user is included in the token's payload, allowing the server to identify the user on subsequent requests.Why Use JWT? JWT allows us to create stateless sessions. Once the user logs in, they receive a token, which can be stored on the client side (usually in local storage or cookies). The server doesn’t need to remember anything about the user between requests, making this approach scalable and efficient.
Middleware for Protecting Routes
To protect certain routes and ensure only authenticated users can access them, create a authMiddleware.js
file in the middleware
folder:
const jwt = require('jsonwebtoken');
const authMiddleware = (req, res, next) => {
const token = req.header('Authorization')?.split(' ')[1];
if (!token) return res.status(401).json({ msg: 'No token, authorization denied' });
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (err) {
res.status(400).json({ msg: 'Token is not valid' });
}
};
module.exports = authMiddleware;
Why Middleware?
The authMiddleware
function intercepts incoming requests to protected routes, checks for a valid JWT in the Authorization header, and decodes the token to verify the user’s identity. If the token is missing or invalid, the middleware returns an error.
By using this middleware, we ensure that only authenticated users can access protected routes, like viewing or updating their profile.
Frontend Setup with React
On the frontend, we'll use React to handle user registration and login forms. The Axios library will be used to make API requests to our backend.
First, install Axios:
npm install axios
Then, create a login.js
component to handle login and save the token to localStorage
:
import React, { useState } from 'react';
import axios from 'axios';
const Login = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleLogin = async () => {
try {
const response = await axios.post('http://localhost:5000/api/auth/login', { email, password });
localStorage.setItem('token', response.data.token);
} catch (err) {
console.error('Error logging in', err);
}
};
return (
<div>
<input type='email' value={email} onChange={(e) => setEmail(e.target.value)} placeholder='Email' />
<input type='password' value={password} onChange={(e) => setPassword(e.target.value)} placeholder='Password' />
<button onClick={handleLogin}>Login</button>
</div>
);
};
export default Login;
Conclusion
JWT authentication is a simple, effective, and scalable method to handle user authentication in modern web applications. By following this guide, you’ve learned how to implement JWT-based authentication in your MERN stack application. You can expand on this by adding features such as user roles, refresh tokens, and token expiration handling.
Keep Learning and keep growing!