JWT authentication is one of the most common ways to secure modern Express.js APIs. In this tutorial, you will learn how to issue JSON Web Tokens, verify them in middleware, protect routes, and follow important security practices.

What is JWT?

JWT (JSON Web Token) is an open standard used to securely transmit information between a client and a server. The token is signed digitally, which allows the receiver to verify that the data has not been changed.

In Express.js applications, JWTs are commonly used for authentication and authorization. After a user logs in, the server issues a token. The client sends that token with future requests, and the server validates it before granting access to protected routes.

A typical JWT flow in a Node.js API looks like this: the user logs in, the server creates a signed token, the client stores it, and the server verifies it in middleware on each protected request.

Why developers use JWT

  1. Stateless authentication: the server does not need to store session data for every user.
  2. Scalable APIs: JWT works well for REST APIs and microservices.
  3. Easy authorization: roles and permissions can be included in the token payload.

JWT Structure

A JWT consists of three parts separated by dots (.):

  1. Header: contains metadata about the token, such as the signing algorithm.
  2. Payload: contains the claims (data) about the user and other information.
  3. Signature: is used to verify that the token has not been tampered with.

For example, a header, payload, and signature might look like this:

header: {"alg": "HS256", "typ": "JWT"}
payload: {"id": 1, "email": "admin@gmail.com", "role": "Admin"}
signature: HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

The server uses a secret key to sign the token. When the client sends the token back, the server can verify it using the same secret key.


Prerequisites

Before you start, make sure you have a basic Express.js project ready. You should also be comfortable with installing packages and using environment variables in Node.js.

  • Node.js and npm installed
  • An Express.js project or a fresh API setup
  • A basic understanding of routes and middleware

Implementing JWT in Express.js

To implement JWT authentication in Express.js, start by installing the jsonwebtoken package. If you use environment variables, installing dotenv is also helpful.

npm install jsonwebtoken dotenv

Create a .env file and store a strong secret value. Keep this value private and never expose it in your source code.

JWT_SECRET=your_strong_secret_here

The following example shows a simple login endpoint that issues a JWT after validating the user credentials.


import express from 'express';
import jwt from 'jsonwebtoken';
import dotenv from 'dotenv';

dotenv.config();

const app = express();
app.use(express.json());

app.get('/api', (req, res) => {
    res.json({
        message: 'Welcome to the API'
    });
});

app.post('/login', (req, res) => {
    const { email, password } = req.body;

    // Normally, validate the user from a database
    if (email === 'admin@gmail.com' && password === '123456') {
        const token = jwt.sign(
            {
                id: 1,
                email,
                role: 'Admin'
            },
            process.env.JWT_SECRET,
            {
                expiresIn: '1h'
            }
        );

        return res.json({
            message: 'Login Successful',
            token
        });
    }

    res.status(401).json({
        message: 'Invalid Email or Password'
    });
});

app.listen(3000, () => {
    console.log('Server running on port 3000');
});

In real applications, you should replace the hardcoded credentials with a database lookup and validate the password securely before issuing a token.


Authentication Middleware

To protect routes, create middleware that reads the Authorization header, extracts the token, and verifies it before allowing access.


import jwt from 'jsonwebtoken';

function authenticate(req, res, next) {
    const authHeader = req.headers.authorization;

    if (!authHeader) {
        return res.status(401).json({
            message: 'Token Missing'
        });
    }

    const token = authHeader.split(' ')[1];

    try {
        const decoded = jwt.verify(token, process.env.JWT_SECRET);
        req.user = decoded;
        next();
    } catch (err) {
        return res.status(401).json({
            message: 'Invalid or Expired Token'
        });
    }
}

module.exports = authenticate;

Protecting Routes

Once the middleware is in place, you can protect any route by adding it before the handler. This pattern is useful when you want only authenticated users to access private resources.


import authenticate from './authenticate';

app.get('/api/protected', authenticate, (req, res) => {
    res.json({
        message: 'This is a protected route',
        user: req.user
    });
});
        

Role-Based Access Control

You can also restrict access based on the user role stored inside the JWT payload. This is a common approach for admin-only endpoints or premium features.


function isAdmin(req, res, next) {
    if (req.user.role !== 'Admin') {
        return res.status(403).json({
            message: 'Access Denied'
        });
    }

    next();
}

Use this middleware together with the authentication middleware when you want to allow only certain roles to access a route.

app.get('/api/admin', authenticate, isAdmin, (req, res) => {
    res.json({
        message: 'This is an admin route',
        user: req.user
    });
});

JWT with cookies

Using HTTP only cookies for token storage. This provides an additional layer of security by preventing client-side JavaScript from accessing the token.

HTTP only cookies are not accessible via JavaScript, making them a secure choice for storing sensitive information like JWTs. In the example below, we'll see how to implement this approach.

Authentication Middleware


import jwt from "jsonwebtoken";

function getCookie(req, name) {
     const cookieHeader = req.headers.cookie;

     if (!cookieHeader) return null;

     const cookies = cookieHeader.split(";").map((cookie) => cookie.trim());
     const match = cookies.find((cookie) => cookie.startsWith(`${name}=`));

     if (!match) return null;

     return decodeURIComponent(match.slice(name.length + 1));
}

export default function authenticate(req,res,next){
     const authHeader=req.headers.authorization;
     const token=authHeader?.startsWith("Bearer ")
          ? authHeader.split(" ")[1]
          : getCookie(req, "token");

     if(!token){
          return res.status(401).json({message: 'Token Missing'})
     }

     try{
          const decode=jwt.verify(token,process.env.JWT_SECRET);
          req.user=decode;
          next();
     }
     catch(err){
          return res.status(401).json({message:'Invalid or Expired Token'})
     }

}

app.js


import express from "express";
import jwt from "jsonwebtoken";

const app=express();
const port=process.env.PORT || 8080;

app.use(express.json());
app.use(express.urlencoded({extended:false}));
app.use(express.static("src/public"));

import authenticate from "./auth.js";


app.get("/edit",authenticate,(req,res)=>{
     res.status(200).json({message: 'This is a protected route',user: req.user});
});

app.post("/login",(req,res)=>{
     const {email,password}=req.body;
   
     if(email=="admin" && password=="123456"){
          const token=jwt.sign({
               id:1,
               email,
               role:"admin"
          },process.env.JWT_SECRET,{
            expiresIn:'1h'   
          });
          
          res.cookie("token", token, {
               httpOnly: true,
               sameSite: "lax",
               maxAge: 60 * 60 * 1000
          });

          return res.status(200).json({message:"success",token, decode:jwt.verify(token,process.env.JWT_SECRET)});
     }
     
     res.status(401).json({message:'Invalid Email or Password'})

});

app.listen(port,()=>{
     console.log(`App running at http://127.0.0.1:${port}`);
});

index.html


<form>
          <label>Email: <input type="email" name="email" required></label>
          <label>Password: <input type="password" name="password" required></label>
          <button>Send</button>
</form>

<script>"use strict";
document.querySelector("form").addEventListener("submit",async function(e){
          e.preventDefault();
          const email=this.email.value;
          const password=this.password.value;

         try{
           const res=await fetch("/login",{
               method:"POST", 
               headers:{"Content-Type":"application/json"}, 
               body:JSON.stringify({email,password})
          });
          if(!res.ok) throw new Error("Request failed with status "+res.status);
          const data=await res.json();
          console.log(data);
          window.location.href = "/edit";
         }
         catch (error) {
          console.warn("Error:", error);
         }
     });
</script>

Best Practices

JWTs are powerful, but they must be used carefully. Follow these guidelines to improve security in your Express.js application.

  • Use short-lived tokens and rotate your secret regularly.
  • Store tokens securely in HTTP-only cookies or protected client storage.
  • Never place sensitive personal data in the payload unless it is necessary.
  • Validate roles and permissions on the server instead of trusting the client.

If you want to log a user out, instruct the client to remove the token from storage. Because JWTs are stateless, the server does not usually keep a session record for them.


Frequently Asked Questions

Is JWT better than sessions? JWT is often preferred for APIs and distributed systems, while sessions are still common for traditional web apps.

Can JWT be used without a database? Yes, but you still need to validate the token and usually store user identity information in the payload or fetch it from a database.

What is the best way to store a JWT? For browser-based apps, HTTP-only cookies are often safer than local storage.