This tutorial teaches you how to build a secure Node.js Express REST API application with JWT Authentication based on roles and MySQL.
1. Introduction#
In this tutorial, we'll build a modern Node.js Express REST API that supports Token-Based Authentication using JWT (JSON Web Tokens) . You'll learn how to:
Implement user signup and login flows with JWT authentication.
Structure a Node.js Express application with CORS, authentication & authorization middlewares, and Sequelize ORM.
Configure Express routes to work seamlessly with JWT.
Define data models and associations for robust authentication and authorization.
Utilize Sequelize to interact with a MySQL database.
By the end of this guide, you'll have a secure and scalable backend ready to support your applications.
2. Understanding Token-Based Authentication#
Token-based authentication offers significant advantages over traditional session-based methods. Instead of storing session information on the server or in cookies, JWTs are stored client-side—such as in Local Storage for web browsers, Keychain for iOS, or SharedPreferences for Android. This approach eliminates the need for additional backend projects or authentication modules for native app users.
JWT Structure#
A JWT consists of three parts:
Header
Payload
Signature
These parts are concatenated as header.payload.signature
. Typically, the client attaches the JWT in the Authorization
header with the Bearer
prefix:
Authorization : Bearer <token>
Alternatively, it can be included in the x-access-token
header:
Subscribe to our Passkeys Substack for the latest news, insights and strategies.
Subscribe
3. Project Overview#
We'll develop a Node.js Express application that allows users to:
Sign Up : Create a new account.
Sign In : Authenticate using username and password.
Role-Based Access : Access resources based on roles (admin, moderator, user).
Our backend will interact with a MySQL database using Sequelize ORM, ensuring efficient data management and security.
4. Technology Stack#
Node.js : Runtime environment.
Express 4 : Web framework.
Sequelize 6 : ORM for MySQL.
MySQL 8 : Relational database.
JWT 9 : Token-based authentication.
bcryptjs 2 : Password hashing.
CORS 2 : Cross-Origin Resource Sharing.
Want to try passkeys yourself? Check our Passkeys Demo.
Try Passkeys
5. Project Structure#
Here's the directory structure for our application:
node-js-jwt-auth/
├── app/
│ ├── config/
│ │ ├── auth.config.js
│ │ └── db.config.js
│ ├── controllers/
│ │ ├── auth.controller.js
│ │ └── user.controller.js
│ ├── middlewares/
│ │ ├── authJwt.js
│ │ └── verifySignUp.js
│ ├── models/
│ │ ├── index.js
│ │ ├── role.model.js
│ │ └── user.model.js
│ └── routes/
│ ├── auth.routes.js
│ └── user.routes.js
├── package.json
└── server.js
6. Setting Up the Node.js Application#
6.1. Initialize the Project#
Create a new directory and initialize the Node.js application:
mkdir node-js-jwt-auth
cd node-js-jwt-auth
npm init -y
6.2. Install Dependencies#
Install the necessary packages:
npm install express sequelize mysql2 cors jsonwebtoken bcryptjs
Ensure your package.json
includes the following script:
"scripts" : {
"start" : "node server.js"
}
Become part of our Passkeys Community for updates and support.
Join
7. Building the Express Server#
7.1. Create server.js
#
Using ESModule syntax, set up the Express server:
// server.js
import express from 'express' ;
import cors from 'cors' ;
import db from './app/models/index.js' ;
import authRoutes from './app/routes/auth.routes.js' ;
import userRoutes from './app/routes/user.routes.js' ;
const app = express ();
const corsOptions = {
origin: 'http://localhost:8081'
};
app. use ( cors (corsOptions));
app. use (express. json ());
app. use (express. urlencoded ({ extended: true }));
// Simple test route
app. get ( '/' , ( req , res ) => {
res. json ({ message: 'Welcome to the Node.js JWT Authentication API.' });
});
// Routes
app. use ( '/api/auth' , authRoutes);
app. use ( '/api/test' , userRoutes);
// Set port and listen for requests
const PORT = process.env. PORT || 8080 ;
db.sequelize. sync ({ force: false }). then (() => {
console. log ( 'Database synchronized' );
app. listen ( PORT , () => {
console. log ( `Server is running on port ${ PORT }.` );
});
});
Note : Ensure that your package.json
includes "type": "module"
to enable ESModule syntax.
7.2. Update package.json
for ESModules#
{
...
"type" : "module" ,
...
}
8. Configuring the Database#
8.1. Create cofiguration file#
Set up your MySQL database configuration in app/config/db.config.js
:
// app/config/db.config.js
export default {
HOST: 'localhost' ,
USER: 'root' ,
PASSWORD: 'root' ,
DB: 'db' ,
PORT: 8081 ,
dialect: 'mysql' ,
pool: {
max: 5 ,
min: 0 ,
acquire: 30000 ,
idle: 10000
}
} ;
8.2. Initialize Sequelize#
Create app/models/index.js
to initialize Sequelize and define model associations:
// app/models/index.js
import Sequelize from 'sequelize' ;
import dbConfig from '../config/db.config.js' ;
import userModel from './user.model.js' ;
import roleModel from './role.model.js' ;
const sequelize = new Sequelize (dbConfig. DB , dbConfig. USER , dbConfig. PASSWORD , {
host: dbConfig. HOST ,
dialect: dbConfig.dialect,
pool: dbConfig.pool,
port: dbConfig. PORT ,
});
const db = {};
db.Sequelize = Sequelize;
db.sequelize = sequelize;
db.user = userModel (sequelize, Sequelize);
db.role = roleModel (sequelize, Sequelize);
db.role. belongsToMany (db.user, {
through: 'user_roles' ,
foreignKey: 'roleId' ,
otherKey: 'userId' ,
});
db.user. belongsToMany (db.role, {
through: 'user_roles' ,
foreignKey: 'userId' ,
otherKey: 'roleId' ,
as: 'roles' ,
});
db. ROLES = [ 'user' , 'admin' , 'moderator' ];
export default db;
8.3. Define Models#
8.3.1. User Model#
// app/models/user.model.js
export default (sequelize, Sequelize) => {
const User = sequelize. define ( 'users' , {
username: {
type: Sequelize. STRING ,
unique: true
},
email: {
type: Sequelize. STRING ,
unique: true
},
password: {
type: Sequelize. STRING
}
});
return User;
};
8.3.2. Role Model#
// app/models/role.model.js
export default (sequelize, Sequelize) => {
const Role = sequelize. define ( 'roles' , {
id: {
type: Sequelize. INTEGER ,
primaryKey: true ,
autoIncrement: true
},
name: {
type: Sequelize. STRING
}
});
return Role;
};
8.4 Local MySQL Database#
If you don't already have some MySQL database for testing, you can create one for your testing environment using the following docker command:
docker run --rm -p 8081:3306 \
-e MYSQL_ROOT_PASSWORD=root \
-e MYSQL_DATABASE=db \
mysql:8
9. Setting Up Authentication Configuration#
9.1. Create a configuration file#
Create a configuration file under app/config/auth.config.js
:
// app/config/auth.config.js
export default {
secret: 'your-secret-key'
} ;
Security Note : Replace 'your-secret-key'
with a strong, unpredictable secret. Consider using environment variables to manage sensitive information.
Want to experiment with passkey flows? Try our Passkeys Debugger.
Try for Free
10. Implementing Middleware Functions#
10.1. Verify Sign-Up Middleware#
Checks for duplicate usernames or emails and validates roles.
// app/middlewares/verifySignUp.js
import db from '../models/index.js' ;
const { ROLES , user : User } = db;
export const checkDuplicateUsernameOrEmail = async ( req , res , next ) => {
try {
const userByUsername = await User. findOne ({
where: { username: req.body.username }
});
if (userByUsername) {
return res. status ( 400 ). json ({ message: 'Username is already in use!' });
}
const userByEmail = await User. findOne ({
where: { email: req.body.email }
});
if (userByEmail) {
return res. status ( 400 ). json ({ message: 'Email is already in use!' });
}
next ();
} catch (error) {
res. status ( 500 ). json ({ message: error.message });
}
};
export const checkRolesExisted = ( req , res , next ) => {
if (req.body.roles) {
for ( const role of req.body.roles) {
if ( ! ROLES . includes (role)) {
return res. status ( 400 ). json ({ message: `Role ${ role } does not exist!` });
}
}
}
next ();
};
10.2. JWT Authentication Middleware#
Verifies tokens and checks user roles.
// app/middlewares/authJwt.js
import jwt from 'jsonwebtoken' ;
import db from '../models/index.js' ;
import authConfig from '../config/auth.config.js' ;
const { user : User , role : Role } = db;
export const verifyToken = async ( req , res , next ) => {
const token = req.headers[ 'x-access-token' ] || req.headers[ 'authorization' ];
if ( ! token) {
return res. status ( 403 ). json ({ message: 'No token provided!' });
}
try {
const decoded = jwt. verify (token. replace ( 'Bearer ' , '' ), authConfig.secret);
req.userId = decoded.id;
const user = await User. findByPk (req.userId);
if ( ! user) {
return res. status ( 401 ). json ({ message: 'Unauthorized!' });
}
next ();
} catch (err) {
return res. status ( 401 ). json ({ message: 'Unauthorized!' });
}
};
export const isAdmin = async ( req , res , next ) => {
try {
const user = await User. findByPk (req.userId);
const roles = await user. getRoles ();
const adminRole = roles. find ( role => role.name === 'admin' );
if (adminRole) {
next ();
return ;
}
res. status ( 403 ). json ({ message: 'Require Admin Role!' });
} catch (error) {
res. status ( 500 ). json ({ message: error.message });
}
};
export const isModerator = async ( req , res , next ) => {
try {
const user = await User. findByPk (req.userId);
const roles = await user. getRoles ();
const modRole = roles. find ( role => role.name === 'moderator' );
if (modRole) {
next ();
return ;
}
res. status ( 403 ). json ({ message: 'Require Moderator Role!' });
} catch (error) {
res. status ( 500 ). json ({ message: error.message });
}
};
export const isModeratorOrAdmin = async ( req , res , next ) => {
try {
const user = await User. findByPk (req.userId);
const roles = await user. getRoles ();
const hasRole = roles. some ( role => [ 'admin' , 'moderator' ]. includes (role.name));
if (hasRole) {
next ();
return ;
}
res. status ( 403 ). json ({ message: 'Require Moderator or Admin Role!' });
} catch (error) {
res. status ( 500 ). json ({ message: error.message });
}
};
10.3. Export Middleware#
// app/middlewares/index.js
import * as authJwt from './authJwt.js' ;
import { checkDuplicateUsernameOrEmail, checkRolesExisted } from './verifySignUp.js' ;
export { authJwt, checkDuplicateUsernameOrEmail, checkRolesExisted };
11. Creating Controllers#
11.1. Authentication Controller#
Handles user signup and signin.
// app/controllers/auth.controller.js
import db from '../models/index.js' ;
import jwt from 'jsonwebtoken' ;
import bcrypt from 'bcryptjs' ;
import authConfig from '../config/auth.config.js' ;
const { user : User , role : Role } = db;
export const signup = async ( req , res ) => {
try {
const { username , email , password , roles } = req.body;
const hashedPassword = await bcrypt. hash (password, 8 );
const userRole = await Role. findOne ({ where: { name: 'user' } });
const user = await User. create ({
username,
email,
password: hashedPassword,
});
await user. setRoles ([userRole]);
res. status ( 201 ). json ({ message: 'User registered successfully!' });
} catch (error) {
res. status ( 500 ). json ({ message: error.message });
}
};
export const signin = async ( req , res ) => {
try {
const { username , password } = req.body;
const user = await User. findOne ({
where: { username },
include: { model: Role, as: 'roles' }
});
if ( ! user) {
return res. status ( 404 ). json ({ message: 'User Not found.' });
}
const passwordIsValid = await bcrypt. compare (password, user.password);
if ( ! passwordIsValid) {
return res. status ( 401 ). json ({
accessToken: null ,
message: 'Invalid Password!'
});
}
const token = jwt. sign ({ id: user.id }, authConfig.secret, {
expiresIn: 86400 // 24 hours
});
const authorities = user.roles. map ( role => `ROLE_${ role . name . toUpperCase () }` );
res. status ( 200 ). json ({
id: user.id,
username: user.username,
email: user.email,
roles: authorities,
accessToken: token
});
} catch (error) {
res. status ( 500 ). json ({ message: error.message });
}
};
11.2. User Controller#
Handles access to protected resources.
// app/controllers/user.controller.js
export const allAccess = ( req , res ) => {
res. status ( 200 ). send ( 'Public Content.' );
};
export const userBoard = ( req , res ) => {
res. status ( 200 ). send ( 'User Content.' );
};
export const adminBoard = ( req , res ) => {
res. status ( 200 ). send ( 'Admin Content.' );
};
export const moderatorBoard = ( req , res ) => {
res. status ( 200 ). send ( 'Moderator Content.' );
};
12. Defining Routes#
12.1. Authentication Routes#
// app/routes/auth.routes.js
import express from 'express' ;
import { signup, signin } from '../controllers/auth.controller.js' ;
import { checkDuplicateUsernameOrEmail, checkRolesExisted } from '../middlewares/verifySignUp.js' ;
const router = express. Router ();
router. post (
'/signup' ,
[checkDuplicateUsernameOrEmail, checkRolesExisted],
signup
);
router. post ( '/signin' , signin);
export default router;
12.2. User Routes#
// app/routes/user.routes.js
import express from 'express' ;
import { allAccess, userBoard, adminBoard, moderatorBoard } from '../controllers/user.controller.js' ;
import { verifyToken, isAdmin, isModerator, isModeratorOrAdmin } from '../middlewares/authJwt.js' ;
const router = express. Router ();
router. get ( '/all' , allAccess);
router. get ( '/user' , [verifyToken], userBoard);
router. get ( '/mod' , [verifyToken, isModerator], moderatorBoard);
router. get ( '/admin' , [verifyToken, isAdmin], adminBoard);
export default router;
13. Running the Application#
13.1. Start the Server#
Use the following command to start your server:
13.2. Testing with Postman#
You can use Postman to test the API endpoints.
13.2.1. Signup a User#
Endpoint : POST /api/auth/signup
Body :
{
"username" : "john_doe" ,
"email" : "john@example.com" ,
"password" : "securePassword123"
}
Note: All users are assigned the user
role by default. More privileged roles should not be obtained through registration,
but directly through database access.
13.2.2. Signin a User#
Endpoint : POST /api/auth/signin
Body :
{
"username" : "john_doe" ,
"password" : "securePassword123"
}
13.2.3. Access Protected Routes#
Use the received accessToken
in the Authorization
header:
Authorization: Bearer <accessToken>
Endpoints :
GET /api/test/all
- Public
GET /api/test/user
- User, Moderator, Admin
GET /api/test/mod
- Moderator
GET /api/test/admin
- Admin
14. Security Best Practices#
Secrets Management : Store sensitive information like JWT secrets and database credentials in environment variables. Utilize packages like dotenv
to manage these variables securely.
Example :
// server.js
import dotenv from 'dotenv' ;
dotenv. config ();
// Access variables using process.env.VARIABLE_NAME
Token Expiry and Refresh Tokens : Implement refresh tokens to maintain user sessions without compromising security. This involves issuing short-lived access tokens and longer-lived refresh tokens.
Secure HTTP Headers : Use middleware like helmet
to set secure HTTP headers.
import helmet from 'helmet' ;
app. use ( helmet ());
Input Validation : Validate all incoming data to prevent attacks. Consider using libraries like express-validator
and integrating with your front-end forms.
15. Conclusion#
Congratulations! You've successfully built a secure and robust Node.js Express REST API with JWT authentication. This setup provides a solid foundation for developing scalable and secure backend services.
Next Steps :
Implement Refresh Tokens : Enhance security by allowing users to refresh their tokens without re-authenticating.
Integrate Frontend : Pair your backend with modern frontend frameworks like React, Vue.js, or Angular.
Deploy to Production : Use platforms like Heroku , Vercel , or AWS to host your application.
Happy coding!
16. Further Reading#