Authentication is the most confusing part of learning the MERN stack.
When I started building full-stack apps, I struggled to understand what actually happens between clicking "Login" and seeing the dashboard. I read tutorials that used NextAuth, Firebase, or Passport.js, but I never understood the core mechanism.
So I decided to build it from scratch. Here is the exact flow of JWT (JSON Web Token) authentication in a MERN app.
The 3-Step Architecture
Authentication is basically a bouncer at a club.
- Verify: Check the ID (Email + Password).
- Issue: Give a wristband (JWT Token).
- Check: Verify the wristband on every request.
Here is how it translates to code.
Step 1: Hashing the Password (Backend)
Never store passwords as plain text. When a user registers, we use bcrypt to hash the password before saving it to MongoDB.
javascript
import bcrypt from "bcryptjs";
import User from "../models/User.js";
// During Registration
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(req.body.password, salt);
const newUser = new User({
email: req.body.email,
password: hashedPassword
});
await newUser.save();
Step 2: Generating the JWT (Backend)
When the user logs in, we compare the entered password with the hashed password. If it matches, we generate a JWT using the jsonwebtoken library.
javascript
import jwt from "jsonwebtoken";
const user = await User.findOne({ email: req.body.email });
if (!user) return res.status(404).json("User not found");
const validPassword = await bcrypt.compare(req.body.password, user.password);
if (!validPassword) return res.status(400).json("Wrong password");
// Create Token
const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET, { expiresIn: "1d" });
res.json({ token, user });
Step 3: Storing the Token (Frontend)
On the React frontend, we receive this token and store it in localStorage.
javascript
const res = await fetch("/api/login", { method: "POST", body: JSON.stringify(data) });
const { token } = await res.json();
// Save the token
localStorage.setItem("token", token);
window.location.href = "/dashboard"; // Redirect
Step 4: Protecting Routes with Middleware
Now, whenever the user visits a protected route (like /dashboard), the frontend must send the token. But the backend needs to verify it. We do this using Middleware.
javascript
import jwt from "jsonwebtoken";
export const verifyToken = (req, res, next) => {
const authHeader = req.headers.token;
if (authHeader) {
jwt.verify(authHeader, process.env.JWT_SECRET, (err, user) => {
if (err) return res.status(403).json("Token is not valid!");
req.user = user;
next(); // Proceed to the actual route
});
} else {
return res.status(401).json("You are not authenticated!");
}
};
Step 5: Using the Middleware
javascript
app.get("/api/dashboard", verifyToken, async (req, res) => {
// req.user now contains the decoded ID from the token
const user = await User.findById(req.user.id);
res.json(user);
});
Why this matters
Tools like NextAuth or Clerk are great, but if you don't understand the underlying JWT mechanism, you will struggle when you need custom logic (like Role-Based Access Control for an Admin Panel).
Building it from scratch takes 2 days, but it gives you 10 years of clarity.
I'm 16 and currently building full-stack projects for real clients. If you are learning MERN, keep pushing.
Follow my journey on X: @Developervxku
Top comments (0)