NIXX/DEVv1.14.0
ArticlesFavorites
Sign In
Sign In
Articles

Welcome to our blog

A curated collection of insightful articles, practical guides, and expert tips designed to simplify your workflow

Cover image for: Building Secure APIs with Node.js and JWT Authentication
October 21, 20256 MIN READ min readBy ℵi✗✗

Building Secure APIs with Node.js and JWT Authentication

Learn how to build secure APIs in Node.js using JWT authentication. Step-by-step guide with code examples to protect routes and handle tokens safely.

Node.jsjwtapi
ℵi✗✗

ℵi✗✗

Full-Stack Developer

Passionate about building tools and sharing knowledge with the developer community.

Was this helpful?

Popular Posts

  • NixOS vs. Arch Linux: Which One Belongs in Your Dev Setup?

    NixOS vs. Arch Linux: Which One Belongs in Your Dev Setup?

    5 MIN READ min read

  • How to Enable HTTPS on Localhost in Under 2 Minutes

    How to Enable HTTPS on Localhost in Under 2 Minutes

    3 MIN READ min read

  • Migrating from Create React App (CRA) to Vite: A Step-by-Step Guide

    Migrating from Create React App (CRA) to Vite: A Step-by-Step Guide

    4 MIN READ min read

  • Array Destructuring in PHP: A Practical Guide for Modern Developers

    Array Destructuring in PHP: A Practical Guide for Modern Developers

    5 MIN READ min read

Recommended Products

  • Apple iPad (7th Gen)

    Apple iPad (7th Gen)

    4.3
  • Fitbit Versa 4

    Fitbit Versa 4

    4.3
  • JBL Flip 6

    JBL Flip 6

    4.8
  • Dell 24 Monitor — SE2425HM Full HD

    Dell 24 Monitor — SE2425HM Full HD

    4.7

May contain affiliate links

Topics

webdev33productivity16cybersecurity12javascript11automation9guide8react7typescript7php6tutorial6freelancing5github actions5privacy5how to4Node.js4
+111 more topics →
🇺🇸USD ACCOUNTOpen a free US-based USD accountReceive & save in USD — powered by ClevaSponsoredInterserver Hosting#1 VALUEAffordable, reliable hosting from $2.50/mo99.9% uptimeSponsored

JWT authentication is one of the most widely used patterns for securing Node.js APIs. The server issues a signed token when a user authenticates, the client sends that token with subsequent requests, and a middleware layer verifies it before the request reaches any protected route handler.

This guide builds a complete implementation: project setup, user registration with hashed passwords, login with token issuance, protected route middleware, and the security practices that matter before moving to production.

What this covers:

  • Project setup with Express, jsonwebtoken, bcryptjs, and dotenv

  • User registration with bcrypt password hashing

  • Login and JWT issuance

  • Authentication middleware for protecting routes

  • Testing the flow with curl

  • Token expiry, refresh tokens, and secure storage


Step 1: Project Setup

mkdir node-jwt-api && cd node-jwt-api
npm init -y
npm install express jsonwebtoken bcryptjs dotenv
npm install --save-dev nodemon

Add a dev script to package.json:

"scripts": {
  "start": "node server.js",
  "dev": "nodemon server.js"
}

Create a .env file:

PORT=4000
JWT_SECRET=replace_this_with_a_long_random_string

The JWT_SECRET should be a randomly generated string of at least 32 characters. A weak or guessable secret undermines the entire security model. Generate one with:

node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

Add .env to .gitignore immediately.


Step 2: Basic Express Server

// server.js
require('dotenv').config();
const express = require('express');

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

const PORT = process.env.PORT || 4000;

app.get('/', (req, res) => {
  res.json({ message: 'API is running' });
});

app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
npm run dev

Step 3: User Registration with Password Hashing

In production, users are stored in a database. For this guide, an in-memory array demonstrates the pattern. The hashing logic is identical regardless of storage.

const bcrypt = require('bcryptjs');

const users = []; // replace with database queries in production

app.post('/register', async (req, res) => {
  const { username, password } = req.body;

  if (!username || !password) {
    return res.status(400).json({ message: 'Username and password are required' });
  }

  const existing = users.find(u => u.username === username);
  if (existing) {
    return res.status(409).json({ message: 'Username already taken' });
  }

  const hashedPassword = await bcrypt.hash(password, 12);
  users.push({ username, password: hashedPassword });

  res.status(201).json({ message: 'User registered successfully' });
});

The salt rounds value of 12 is a reasonable balance between security and performance. The OWASP password storage cheat sheet recommends a minimum of 10 rounds; 12 is a common production choice. Higher values increase the time to hash and verify, which is desirable for slowing brute-force attempts.

The duplicate username check prevents a user from registering the same username twice, which would make login ambiguous.


Step 4: Login and Token Issuance

const jwt = require('jsonwebtoken');

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

  if (!username || !password) {
    return res.status(400).json({ message: 'Username and password are required' });
  }

  const user = users.find(u => u.username === username);

  // Use the same error message for "not found" and "wrong password"
  if (!user) {
    return res.status(401).json({ message: 'Invalid credentials' });
  }

  const isValid = await bcrypt.compare(password, user.password);
  if (!isValid) {
    return res.status(401).json({ message: 'Invalid credentials' });
  }

  const token = jwt.sign(
    { username: user.username },
    process.env.JWT_SECRET,
    { expiresIn: '15m' }
  );

  res.json({ token });
});

Two security details worth noting:

The error message is identical for "user not found" and "wrong password". Distinct messages (User not found vs Invalid password) allow an attacker to enumerate valid usernames. Using the same message for both prevents this.

expiresIn: '15m' is intentionally short. A stolen 1-hour token is a 1-hour window for an attacker. A stolen 15-minute token is a much shorter window. Pair short access tokens with refresh tokens (covered below) to maintain sessions without requiring frequent logins.


Step 5: Authentication Middleware

function authenticateToken(req, res, next) {
  const authHeader = req.headers['authorization'];
  const token = authHeader?.split(' ')[1]; // "Bearer <token>"

  if (!token) {
    return res.status(401).json({ message: 'Access token required' });
  }

  jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
    if (err) {
      const message =
        err.name === 'TokenExpiredError'
          ? 'Token expired'
          : 'Invalid token';
      return res.status(401).json({ message });
    }

    req.user = decoded;
    next();
  });
}

Distinguishing TokenExpiredError from other verification failures allows the client to handle each case appropriately: an expired token warrants a refresh attempt; an invalid token warrants re-authentication.

Apply the middleware to protected routes:

app.get('/profile', authenticateToken, (req, res) => {
  res.json({ username: req.user.username });
});

For routes where the middleware applies to every handler in a group, apply it at the router level:

const protectedRouter = express.Router();
protectedRouter.use(authenticateToken);

protectedRouter.get('/profile', (req, res) => {
  res.json({ username: req.user.username });
});

protectedRouter.get('/settings', (req, res) => {
  res.json({ /* settings data */ });
});

app.use('/api', protectedRouter);

Step 6: Test the Flow with curl

Register

curl -X POST http://localhost:4000/register \
  -H "Content-Type: application/json" \
  -d '{"username":"alice","password":"mypassword"}'

Expected response:

{ "message": "User registered successfully" }

Login

curl -X POST http://localhost:4000/login \
  -H "Content-Type: application/json" \
  -d '{"username":"alice","password":"mypassword"}'

Expected response:

{ "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." }

Access a Protected Route

curl http://localhost:4000/profile \
  -H "Authorization: Bearer <token>"

Expected response:

{ "username": "alice" }

Without a token: 401 with Access token required. With an expired token: 401 with Token expired. With a tampered token: 401 with Invalid token.


Step 7: Refresh Tokens

With short-lived access tokens, a mechanism for issuing new access tokens without requiring re-login is necessary. Refresh tokens are long-lived tokens that are stored in httpOnly cookies and exchanged for new access tokens.

// On login, issue both an access token and a refresh token
app.post('/login', async (req, res) => {
  // ... validation and password check ...

  const accessToken = jwt.sign(
    { username: user.username },
    process.env.JWT_SECRET,
    { expiresIn: '15m' }
  );

  const refreshToken = jwt.sign(
    { username: user.username },
    process.env.REFRESH_SECRET,
    { expiresIn: '7d' }
  );

  // Store the refresh token (database in production, not in-memory)
  user.refreshToken = refreshToken;

  res.cookie('refreshToken', refreshToken, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'strict',
    maxAge: 7 * 24 * 60 * 60 * 1000,
  });

  res.json({ accessToken });
});
// Refresh endpoint
app.post('/refresh', (req, res) => {
  const refreshToken = req.cookies.refreshToken;
  if (!refreshToken) return res.sendStatus(401);

  jwt.verify(refreshToken, process.env.REFRESH_SECRET, (err, decoded) => {
    if (err) return res.sendStatus(403);

    const newAccessToken = jwt.sign(
      { username: decoded.username },
      process.env.JWT_SECRET,
      { expiresIn: '15m' }
    );

    res.json({ accessToken: newAccessToken });
  });
});
// Logout — clear the refresh token
app.post('/logout', (req, res) => {
  const refreshToken = req.cookies.refreshToken;

  if (refreshToken) {
    const user = users.find(u => u.refreshToken === refreshToken);
    if (user) user.refreshToken = null;
  }

  res.clearCookie('refreshToken');
  res.json({ message: 'Logged out' });
});

Add cookie-parser to read cookies:

npm install cookie-parser
const cookieParser = require('cookie-parser');
app.use(cookieParser());

Add REFRESH_SECRET to .env — use a different value from JWT_SECRET.


Security Considerations

Token payload. JWTs are base64-encoded, not encrypted. Anyone with the token can read the payload. Include only the minimum necessary data (user ID, role). Never include passwords, sensitive personal information, or anything that should remain private.

httpOnly cookies vs localStorage. Storing access tokens in localStorage exposes them to XSS attacks — any script running on the page can read localStorage. Storing access tokens in memory (React state) and refresh tokens in httpOnly cookies is the more secure pattern for web applications. httpOnly cookies are inaccessible to JavaScript.

Token invalidation. JWTs are stateless and cannot be revoked before they expire. For applications where immediate invalidation matters (account deletion, password change), maintain a server-side blocklist of invalidated token IDs (jti claim). Check the blocklist in the middleware.

Rate limiting. Login endpoints are targets for brute-force attacks. Apply rate limiting to /login and /register:

npm install express-rate-limit
const rateLimit = require('express-rate-limit');

const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 10,
  message: { message: 'Too many login attempts. Try again later.' },
});

app.post('/login', loginLimiter, async (req, res) => {
  /* ... */
});

Key Takeaways

  • Generate JWT_SECRET and REFRESH_SECRET with a cryptographically random generator. Store them in .env, never in code.

  • Use bcrypt with at least 10 salt rounds (12 is a reasonable production value) for password hashing.

  • Return identical error messages for "user not found" and "wrong password" to prevent username enumeration.

  • Keep access tokens short-lived (15 minutes). Use refresh tokens stored in httpOnly cookies for session continuity.

  • Distinguish TokenExpiredError from other JWT errors in middleware so clients can attempt a refresh rather than immediately redirecting to login.

  • Apply rate limiting to authentication endpoints to slow brute-force attempts.

  • Never put sensitive data in the JWT payload — it is base64-encoded and readable by anyone with the token.


Conclusion

The pattern built here — bcrypt for password hashing, JWT for stateless authentication, middleware for route protection, and refresh tokens for session continuity — covers the core requirements of a secure Node.js API. Each piece addresses a specific threat: hashing protects stored passwords, short-lived tokens limit exposure windows, middleware centralizes verification, and refresh tokens avoid requiring re-login on every expiry.

Moving to production means replacing the in-memory user store with a real database, adding rate limiting, and reviewing the token storage approach for the client type being served.


Building this for a specific framework or hitting a specific authentication edge case? Share it in the comments.

Topics
Node.jsjwtapi
Interserver Hosting#1 VALUEAffordable, reliable hosting from $2.50/mo99.9% uptimeSponsored

Discussion

Join the discussion

Sign in to share your thoughts and engage with the community.

Sign In
Loading comments…

Continue Reading

More Articles

View all
Cover image for: The 3-Device Rule: How to Simplify Your Digital Life and Stop Overbuying Tech
Aug 5, 20255 MIN READ min read

The 3-Device Rule: How to Simplify Your Digital Life and Stop Overbuying Tech

Tired of juggling too many devices? Learn the 3-device rule that helps you streamline your digital life, reduce clutter, and focus on what really matters.

Cover image for: What Is Identity Theft (and How to Protect Yourself Online)
Nov 17, 20256 MIN READ min read

What Is Identity Theft (and How to Protect Yourself Online)

Identity theft can happen to anyone — often without you even realizing it. Learn what it means, how it happens, and the smart steps you can take today to keep your personal information safe online.

Cover image for: AI for DevOps: Tools That Are Already Changing the Game
Jun 17, 20256 MIN READ min read

AI for DevOps: Tools That Are Already Changing the Game

How artificial intelligence is transforming CI/CD pipelines, monitoring, and incident response—today.

Cover image for: React Authentication with JWT: A Step-by-Step Guide
Oct 17, 20257 MIN READ min read

React Authentication with JWT: A Step-by-Step Guide

Learn how to implement secure JWT authentication in React. From login to route protection and API calls, this guide covers everything you need to know.

|Made with · © 2026|TermsPrivacy
AboutBlogContact

Free, open-source tools for developers and creators · Community driven