When building modern web applications with the MERN stack (MongoDB, Express.js, React, Node.js), you often need to differentiate between pages that anyone can access (Public Routes) and those that require user authentication (Private or Protected Routes).
This post explains everything step-by-step, including real-time examples and code to help you implement it properly.
1. Real-Time Use Case Scenario
Imagine you're building a sports e-commerce website. Here's a simplified route structure:
Public Routes:
-
/
→ Home Page -
/login
→ Login Page -
/register
→ Register Page -
/shop
→ View all products -
/product/:id
→ Product details
Private (Protected) Routes:
-
/cart
→ Only for logged-in users -
/wishlist
→ Only for logged-in users -
/dashboard
→ User’s personal area -
/admin
→ Admin-only section
We want to prevent unauthenticated users from accessing /cart
, /wishlist
, or /dashboard
.
2. Folder Structure (Client-Side)
/src
/pages
Home.tsx
Login.tsx
Register.tsx
Cart.tsx
Wishlist.tsx
Dashboard.tsx
/components
PrivateRoute.tsx
/App.tsx
3. Setting Up React Router v6 in App.tsx
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Home from "./pages/Home";
import Login from "./pages/Login";
import Register from "./pages/Register";
import Cart from "./pages/Cart";
import Wishlist from "./pages/Wishlist";
import Dashboard from "./pages/Dashboard";
import PrivateRoute from "./components/PrivateRoute";
function App() {
return (
<BrowserRouter>
<Routes>
{/* Public Routes */}
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
{/* Private Routes */}
<Route element={<PrivateRoute />}>
<Route path="/cart" element={<Cart />} />
<Route path="/wishlist" element={<Wishlist />} />
<Route path="/dashboard" element={<Dashboard />} />
</Route>
</Routes>
</BrowserRouter>
);
}
export default App;
4. Creating the PrivateRoute
Component
React Router v6 no longer uses render
or component
props. Instead, we use the Outlet
component to wrap child routes.
PrivateRoute.tsx:
import { Navigate, Outlet } from "react-router-dom";
const PrivateRoute = () => {
const token = localStorage.getItem("token");
return token ? <Outlet /> : <Navigate to="/login" />;
};
export default PrivateRoute;
Explanation:
- If
token
exists, the user is considered authenticated and can access child routes (cart
,wishlist
, etc.). - If not, they are redirected to
/login
.
5. Example Login Flow (Client + Server Integration)
Backend: POST /api/login
- Validates user credentials
- Returns a JWT token if successful
Frontend Login Submit Example:
const handleLogin = async () => {
const response = await fetch("http://localhost:5000/api/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password }),
});
const data = await response.json();
if (data.token) {
localStorage.setItem("token", data.token);
navigate("/dashboard");
} else {
alert("Invalid credentials");
}
};
6. Backend Middleware for Token Validation
authMiddleware.js:
const jwt = require("jsonwebtoken");
const authMiddleware = (req, res, next) => {
const token = req.headers.authorization?.split(" ")[1];
if (!token) {
return res.status(401).json({ message: "Access Denied: No Token" });
}
try {
const verified = jwt.verify(token, process.env.JWT_SECRET);
req.user = verified;
next();
} catch (err) {
res.status(401).json({ message: "Invalid Token" });
}
};
module.exports = authMiddleware;
Usage in Routes (Express.js):
const auth = require("./middleware/authMiddleware");
router.get("/user/cart", auth, (req, res) => {
// only logged-in users can access this
});
7. Axios Setup to Send Token Automatically (Frontend)
import axios from "axios";
axios.defaults.baseURL = "http://localhost:5000/api";
axios.interceptors.request.use((config) => {
const token = localStorage.getItem("token");
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
8. Optional: Logout Functionality
const handleLogout = () => {
localStorage.removeItem("token");
navigate("/login");
};
9. Summary Table
Route | Type | Access Condition | Redirect If Not Logged In |
---|---|---|---|
/ |
Public | Always available | - |
/login |
Public | Always available | - |
/cart |
Private | Requires token | Redirect to /login
|
/dashboard |
Private | Requires token | Redirect to /login
|
/admin |
Private | Requires token + admin role | Redirect or deny access |
10. Final Thought
Implementing Public and Private routes in React Router v6 is critical for security, user experience, and clean app architecture. In a MERN stack app, this approach ensures your frontend is aligned with your backend authentication.
When done correctly, you:
- Prevent unauthorized access
- Improve security
- Maintain clean separation between user roles and route access
Use this structure as a base to expand into features like role-based routes, session expiration, or even token refresh strategies.
Top comments (0)