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 ![Why Object.keys(user) Returns string[], Not (keyof User)[]: TypeScript Means It](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2q4w48b9bzs43tynyhsf.png)
