Why Do We Need Authentication?
In any real-world application, you have data or pages that shouldn't be public. You need a way to:
Authenticate a user (prove they are who they say they are, usually with a login).
Authorize a user (check if they have permission to access a specific resource, like their own dashboard).
While old methods used server-side sessions, the modern, stateless way to do this is with JSON Web Tokens (JWT).
What is a JWT?
A JWT is just a long, secure string. This string is an open standard (RFC 7519) that safely transmits information between two parties (like your backend server and your frontend app).
A JWT consists of three parts separated by dots (.):
Header: Contains the token type (JWT) and the signing algorithm (like HS256).
Payload: Contains the data (called "claims"). This is where you'd put user information, like userId or email.
Signature: This is a cryptographic signature. Your server uses a secret key to create this. If the user changes anything in the payload, the signature will become invalid, and your server will reject the token.
This "stateless" approach is powerful: your server doesn't need to store session information. It just needs to check the token's signature to trust the data inside it.
Part 1: The Backend (Node.js & Express.js)
Let's build the API endpoints for registration, login, and a protected route.
What you'll need: npm install express jsonwebtoken bcryptjs
jsonwebtoken: The library for creating and verifying tokens.
bcryptjs: The library for securely "hashing" (scrambling) passwords before saving them. Never store plain-text passwords!
Step 1: User Registration (/register)
When a user signs up, you hash their password and save them to your database (e.g., using PostgreSQL and Prisma).
JavaScript
// /routes/auth.js
const express = require('express');
const bcrypt = require('bcryptjs');
const prisma = require('../prismaClient'); // Assuming you have Prisma set up
const router = express.Router();
router.post('/register', async (req, res) => {
const { email, password } = req.body;
// Hash the password
const hashedPassword = await bcrypt.hash(password, 10);
try {
const user = await prisma.user.create({
data: {
email,
password: hashedPassword,
},
});
res.status(201).json({ message: 'User created successfully' });
} catch (error) {
res.status(400).json({ error: 'Email already exists' });
}
});
module.exports = router;
Step 2: User Login (/login)
When a user logs in, you find them, check their password, and—if it's correct—you create and send them a JWT.
JavaScript
// /routes/auth.js (continued)
const jwt = require('jsonwebtoken');
// !! IMPORTANT: Store your secret in a .env file, not in the code!
const JWT_SECRET = process.env.JWT_SECRET || 'your-very-secret-key-that-is-long';
router.post('/login', async (req, res) => {
const { email, password } = req.body;
// 1. Find the user
const user = await prisma.user.findUnique({
where: { email },
});
if (!user) {
return res.status(400).json({ error: 'Invalid email or password' });
}
// 2. Check the password
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
return res.status(400).json({ error: 'Invalid email or password' });
}
// 3. Create the JWT
const token = jwt.sign(
{
userId: user.id,
email: user.email
},
JWT_SECRET,
{ expiresIn: '1h' } // Token expires in 1 hour
);
// 4. Send the token
res.json({
message: 'Login successful',
token: token,
});
});
Step 3: Auth Middleware (To Protect Routes)
This is the most important part. We need a "gatekeeper" function (middleware) to check for a valid token on any route we want to protect.
JavaScript
// /middleware/auth.js
const jwt = require('jsonwebtoken');
const JWT_SECRET = process.env.JWT_SECRET;
const authMiddleware = (req, res, next) => {
// 1. Get the token from the header
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Access denied. No token provided.' });
}
const token = authHeader.split(' ')[1]; // "Bearer TOKEN"
// 2. Verify the token
try {
const decoded = jwt.verify(token, JWT_SECRET);
req.user = decoded; // Add the user payload to the request object
next(); // Move to the next function (the protected route)
} catch (ex) {
res.status(400).json({ error: 'Invalid token.' });
}
};
module.exports = authMiddleware;
Now, you can easily protect any route:
JavaScript
// /routes/protected.js
const express = require('express');
const router = express.Router();
const authMiddleware = require('../middleware/auth');
// This route is now protected!
router.get('/profile', authMiddleware, (req, res) => {
// Because of the middleware, we have access to req.user
res.json({
message: `Welcome to your profile, ${req.user.email}`,
userId: req.user.userId,
});
});
module.exports = router;
Part 2: The Frontend (React/Next.js)
Your frontend is now responsible for:
Storing the token after login.
Sending the token with every request to a protected route.
Redirecting users who aren't logged in.
Step 1: Login and Store the Token
When your login form is submitted, you call your API, get the token, and store it in localStorage.
JavaScript
// /pages/login.js
import { useState } from 'react';
import axios from 'axios';
import { useRouter } from 'next/router';
export default function Login() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const router = useRouter();
const handleSubmit = async (e) => {
e.preventDefault();
try {
const res = await axios.post('/api/login', { email, password });
// 1. Store the token
localStorage.setItem('token', res.data.token);
// 2. Redirect to the protected page
router.push('/dashboard');
} catch (error) {
console.error('Login failed', error);
}
};
// ... return login form JSX
}
Step 2: Send the Token with API Requests
For any API call to a protected endpoint, you must include the token in the Authorization header.
A great way to do this is to create a central axios instance (e.g., in /lib/api.js).
JavaScript
// /lib/api.js
import axios from 'axios';
const api = axios.create({
baseURL: '/api', // Your API base URL
});
// Interceptor: This runs BEFORE each request
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
if (token) {
// Set the auth header
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
export default api;
Now, you can import this api instance instead of axios everywhere, and the token will be attached automatically!
JavaScript
// /pages/dashboard.js
import { useEffect, useState } from 'react';
import api from '../lib/api'; // <-- Import our custom instance
export default function Dashboard() {
const [profile, setProfile] = useState(null);
useEffect(() => {
api.get('/profile') // The token is auto-attached!
.then(res => setProfile(res.data))
.catch(err => {
// If token is invalid or expired, server sends 400/401
console.error(err);
// We should redirect to login here
});
}, []);
if (!profile) return <div>Loading...</div>;
return <h1>{profile.message}</h1>;
}
Step 3: Handle Logout
Logout is simple: just remove the token and redirect.
JavaScript
// /components/Navbar.js
import { useRouter } from 'next/router';
export default function Navbar() {
const router = useRouter();
const handleLogout = () => {
// 1. Remove the token
localStorage.removeItem('token');
// 2. Redirect to login
router.push('/login');
};
return (
<nav>
{/* ... other links */}
<button onClick={handleLogout}>Logout</button>
</nav>
);
}
Conclusion & Next Steps
You've now implemented a full-stack authentication flow!
Backend: Creates a secure token on login and verifies it on protected routes using middleware.
Frontend: Stores the token on login, attaches it to all API requests, and removes it on logout.
Security Note: Storing the token in localStorage is common, but for maximum security, you should use HttpOnly cookies. This prevents malicious JavaScript from stealing the token (XSS attacks). This is a more complex setup but is the industry best practice.
The Full-Stack Guide to JWT Authentication (with Node.js and React)
•By Md. Abu Sufian

Tags
#Authentication#JWT#Node.js#Express.js#React#Next.js#Web Development#Security#Full-Stack