DEV Community

Prajesh Prajapati
Prajesh Prajapati

Posted on

Private (Protected) and Public Routes in React Router v6 – with Real-Time MERN Stack Example

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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");
  }
};
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Usage in Routes (Express.js):

const auth = require("./middleware/authMiddleware");

router.get("/user/cart", auth, (req, res) => {
  // only logged-in users can access this
});
Enter fullscreen mode Exit fullscreen mode

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;
});
Enter fullscreen mode Exit fullscreen mode

8. Optional: Logout Functionality

const handleLogout = () => {
  localStorage.removeItem("token");
  navigate("/login");
};
Enter fullscreen mode Exit fullscreen mode

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)