How to protect Express routes in Node.js



This content originally appeared on DEV Community and was authored by Jahongir Sobirov

Securing routes is critical in modern web applications. In this tutorial, we’ll learn how to use auth-verify, a powerful Node.js authentication library, to protect Express routes using JWT middleware.
Firstly we need to install all essential packages:

npm install express ejs body-parser auth-verify path

Folder structure:

web-app/
├─ views/
│  ├─ login.ejs
│  ├─ register.ejs
│  └─ dashboard.ejs
├─ public/
│  └─ style.css
├─ index.js

So let’s make our index.js:

const express = require("express");
const bodyParser = require("body-parser");
const AuthVerify = require("auth-verify");
const path = require("path");

const app = express();
const PORT = 3000;

// Serve CSS
app.use(express.static(path.join(__dirname, "public")));

// Middlewares
app.use(bodyParser.urlencoded({ extended: true }));
app.set("view engine", "ejs");

// Initialize AuthVerify
const auth = new AuthVerify({ jwtSecret: "supersecret", storeTokens: "memory" });

// In-memory "database"
const users = []; // { username, password, role }

// Start server
app.listen(PORT, () => console.log(`Server running at http://localhost:${PORT}`));

Making routes

We’ll make pages and routes for registration, login, logoutm, profile and admin-profile
And so let’s make register page:

// Routes

// Register page
app.get("/register", (req, res) => res.render("register"));

// Handle registration
app.post("/register", async (req, res) => {
  const { username, password, role } = req.body;
  if (users.find(u => u.username === username)) {
    return res.send("User already exists. <a href='/register'>Try again</a>");
  }

  users.push({ username, password, role });
  res.send("Registered! <a href='/login'>Login</a>");
});

Let’s make login route:

// Login page
app.get("/login", (req, res) => res.render("login"));

// Handle login
app.post("/login", async (req, res) => {
  const { username, password } = req.body;
  const user = users.find(u => u.username === username && u.password === password);
  if (!user) return res.send("Invalid credentials. <a href='/login'>Try again</a>");

  // Sign JWT and set cookie automatically (auth-verify handles cookies)
  await auth.jwt.sign({ username: user.username, role: user.role }, "1h", { res });
  res.redirect("/dashboard");
});

Logout part:

// Logout
app.get("/logout", (req, res) => {
  res.clearCookie(auth.jwt.cookieName || "authToken");
  res.redirect("/login");
});

And now our main task is protect profile and admin-only route with middleware:

// Protected dashboard
app.get("/dashboard", auth.jwt.protect(), (req, res) => {
  res.render("dashboard", { user: req.user });
});

// Admin-only page
app.get("/admin", auth.jwt.protect({ requiredRole: "admin" }), (req, res) => {
  res.send(`Hello Admin ${req.user.username}`);
});

EJS templates

We’ll make registration page:
views/register.ejs

<!DOCTYPE html>
<html>
<head>
    <title>Register</title>
    <link rel="stylesheet" href="/style.css">
</head>
<body>
    <div class="container">
        <h1>Register</h1>
        <form action="/register" method="post">
            <input type="text" name="username" placeholder="Username" required/>
            <input type="password" name="password" placeholder="Password" required/>
            <select name="role">
                <option value="user">User</option>
                <option value="admin">Admin</option>
            </select>
            <button type="submit">Register</button>
        </form>
        <a href="/login">Already have an account? Login</a>
    </div>
</body>
</html>

Login page also:
views/login.ejs:

<!DOCTYPE html>
<html>
<head>
    <title>Login</title>
    <link rel="stylesheet" href="/style.css">
</head>
<body>
    <div class="container">
        <h1>Login</h1>
        <form action="/login" method="post">
            <input type="text" name="username" placeholder="Username" required/>
            <input type="password" name="password" placeholder="Password" required/>
            <button type="submit">Login</button>
        </form>
        <a href="/register">Don't have an account? Register</a>
    </div>
</body>
</html>

Profile page:
views/dashboard.ejs:

<!DOCTYPE html>
<html>
<head>
    <title>Dashboard</title>
    <link rel="stylesheet" href="/style.css">
</head>
<body>
    <div class="container">
        <h1>Dashboard</h1>
        <h2>Welcome <%= user.username %>!</h2>
        <p>Role: <%= user.role %></p>
        <a href="/logout">Logout</a>
    </div>
</body>
</html>

CSS styling

public/style.css

body {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    background: linear-gradient(120deg, #84fab0, #8fd3f4);
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
}

.container {
    background: white;
    padding: 40px 50px;
    border-radius: 12px;
    box-shadow: 0 10px 25px rgba(0,0,0,0.2);
    width: 350px;
    text-align: center;
}

h1 {
    margin-bottom: 20px;
    color: #333;
}

input, select {
    width: 100%;
    padding: 12px 10px;
    margin: 10px 0;
    border: 1px solid #ccc;
    border-radius: 8px;
}

button {
    width: 100%;
    padding: 12px;
    background: #4facfe;
    background: linear-gradient(to right, #00f2fe, #4facfe);
    border: none;
    color: white;
    font-weight: bold;
    border-radius: 8px;
    cursor: pointer;
    margin-top: 10px;
    transition: 0.3s ease;
}

button:hover {
    opacity: 0.9;
}

a {
    display: inline-block;
    margin-top: 15px;
    color: #4facfe;
    text-decoration: none;
}

And we’ll run our app:

node index.js

And the result:

And full index.js:

const express = require("express");
const bodyParser = require("body-parser");
const AuthVerify = require("auth-verify");
const path = require("path");

const app = express();
const PORT = 3000;

// Serve CSS
app.use(express.static(path.join(__dirname, "public")));

// Middlewares
app.use(bodyParser.urlencoded({ extended: true }));
app.set("view engine", "ejs");

// Initialize AuthVerify
const auth = new AuthVerify({ jwtSecret: "supersecret", storeTokens: "memory" });

// In-memory "database"
const users = []; // { username, password, role }

// Routes

// Register page
app.get("/register", (req, res) => res.render("register"));

// Handle registration
app.post("/register", async (req, res) => {
  const { username, password, role } = req.body;
  if (users.find(u => u.username === username)) {
    return res.send("User already exists. <a href='/register'>Try again</a>");
  }

  users.push({ username, password, role });
  res.send("Registered! <a href='/login'>Login</a>");
});

// Login page
app.get("/login", (req, res) => res.render("login"));

// Handle login
app.post("/login", async (req, res) => {
  const { username, password } = req.body;
  const user = users.find(u => u.username === username && u.password === password);
  if (!user) return res.send("Invalid credentials. <a href='/login'>Try again</a>");

  // Sign JWT and set cookie automatically (auth-verify handles cookies)
  await auth.jwt.sign({ username: user.username, role: user.role }, "1h", { res });
  res.redirect("/dashboard");
});

// Logout
app.get("/logout", (req, res) => {
  res.clearCookie(auth.jwt.cookieName || "authToken");
  res.redirect("/login");
});

// Protected dashboard
app.get("/dashboard", auth.jwt.protect(), (req, res) => {
  res.render("dashboard", { user: req.user });
});

// Admin-only page
app.get("/admin", auth.jwt.protect({ requiredRole: "admin" }), (req, res) => {
  res.send(`Hello Admin ${req.user.username}`);
});

// Home page
app.get("/", (req, res) => res.redirect("/login"));

// Start server
app.listen(PORT, () => console.log(`Server running at http://localhost:${PORT}`));


This content originally appeared on DEV Community and was authored by Jahongir Sobirov