In today’s digital world, securing your application's data is as important as building its features. REST APIs are the backbone of modern web and mobile applications, and without proper authentication and authorization, your app is vulnerable to attacks.
This tutorial will walk you through building a secure RESTful API using Node.js, Express.js, MongoDB, and JWT (JSON Web Tokens). You’ll learn how to implement essential security measures such as password hashing, token-based authentication, and route protection.
By the end of this guide, you’ll be able to:
- Set up a basic Express server and connect to MongoDB
- Register and authenticate users securely using JWT
- Protect your API endpoints with middleware
- Understand and implement security best practices
Whether you’re building a full-stack app, a mobile backend, or a microservice, these are the foundational skills you need to keep your users and data safe.
Let’s get started!
1. Prerequisites
Before we begin building the secure REST API, make sure you have the following tools and knowledge in place:
Tools and Software
- Node.js and npm: Ensure Node.js https://nodejs.org (v14 or later) and npm are installed. You can download it from nodejs.org.
- MongoDB: Either install MongoDB locally or use MongoDB Atlas https://www.mongodb.com/products/platform/atlas-database for a free cloud-based solution.
- Postman or cURL: For testing your API endpoints.
Basic Knowledge
- JavaScript (ES6+): Familiarity with modern JavaScript syntax.
- REST APIs: Understanding of REST principles like HTTP methods (GET, POST, PUT, DELETE) and status codes.
- JSON: Ability to work with JSON data format.
If you’re comfortable with the above, you’re ready to start building the project. In the next section, we’ll set up the project and install all the dependencies we’ll need.
2. Project Setup
Let’s start by creating the project structure and installing the required dependencies
Step 1: Initialize the Project
Open your terminal and run the following commands:
mkdir secure-node-api
cd secure-node-api
npm init -y
This will create a new project folder with a basic package.json file.
Step 2: Install Dependencies
Now install the necessary packages:
npm install express mongoose bcryptjs jsonwebtoken dotenv cors
And install nodemon as a development dependency to auto-restart the server on changes:
Step 3: Update package.json Scripts
In your package.json, replace the scripts section with:
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
},
Step 4: Project Folder Structure
Create the following folder and file structure:
secure-node-api/
├── config/
│ └── db.js
├── middleware/
│ └── auth.js
├── models/
│ └── User.js
├── routes/
│ └── auth.js
├── .env
├── server.js
You can create these manually or use the command line:
mkdir config middleware models routes
touch config/db.js middleware/auth.js models/User.js routes/auth.js server.js .env
Step 5: Add Basic Server Code
In server.js, add the following starter code:
import express, { json } from "express";
import { connect } from "mongoose";
import { config } from "dotenv";
import cors from "cors";
// Load environment variables
config();
const app = express();
app.use(cors());
app.use(json());
// Routes
app.use("/api/auth", require("./routes/auth"));
// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
That’s it for the project setup! In the next section, we’ll connect to MongoDB and prepare the database for storing users securely.
3. Connect to MongoDB
In this step, we'll configure a connection to MongoDB using Mongoose. You can use either a local MongoDB instance or a free MongoDB Atlas https://www.mongodb.com/products/platform/atlas-database cluster.
Step 1: Create a .env File
Add your MongoDB connection string and any other secrets to .env:
PORT=3000
MONGO_URI=mongodb://localhost:27017/secureapi
JWT_SECRET=your_jwt_secret_key_here
🔐 Replace your_jwt_secret_key_here with a strong, random string. If you're using MongoDB Atlas, use the connection string provided in your Atlas dashboard.
Step 2: Set Up the Database Connection
In config/db.js, add the following:
import mongoose from "mongoose";
const connectDB = async () => {
try {
await mongoose.connect(process.env.MONGO_URI, {
useNewUrlParser: true,
useUnifiedTopology: true
});
console.log("MongoDB connected");
} catch (err) {
console.error(err.message);
process.exit(1);
}
};
export default connectDB;
Step 3: Update server.js to use the DB Connection
At the top of server.js, import the connectDB function:
import connectDB from "./config/db.js";
Then, call it before starting the server:
connectDB();
Your updated server.js should look like this:
import express, { json } from "express";
import { config } from "dotenv";
import cors from "cors";
import connectDB from "./config/db.js";
// Load environment variables
config();
connectDB();
const app = express();
app.use(cors());
app.use(json());
// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
At this point, your server should be successfully connecting to MongoDB. You can test it by running:
npm run dev
If everything is configured correctly, you’ll see:
MongoDB connected
Server running on port 5000
4. Create the User Model
In this section, we’ll define a User schema for MongoDB using Mongoose. This model will store user details such as name, email, and a hashed password.
Step 1: Create the User Schema
In models/User.js, add the following code:
import mongoose from 'mongoose';
const UserSchema = new mongoose.Schema({
name: {
type: String,
required: true,
trim: true
},
email: {
type: String,
required: true,
unique: true,
lowercase: true
},
password: {
type: String,
required: true
}
}, {
timestamps: true
});
export default mongoose.model('User', UserSchema);
Schema Fields Explained
- name: Full name of the user.
- email: Unique email address used for login.
- password: Hashed password stored securely.
- timestamps: Automatically adds createdAt and updatedAt fields.
Step 2: Install Mongoose Validation Plugin (Optional)
For cleaner error messages on duplicate emails or schema violations, consider installing mongoose-unique-validator:
npm install --legacy-peer-deps mongoose-unique-validator
Then update your model like this:
import mongoose from 'mongoose';
import uniqueValidator from 'mongoose-unique-validator';
const UserSchema = new mongoose.Schema({
name: { type: String, required: true, trim: true },
email: { type: String, required: true, unique: true, lowercase: true },
password: { type: String, required: true }
}, { timestamps: true });
UserSchema.plugin(uniqueValidator);
export default mongoose.model('User', UserSchema);
Now you have a fully defined User model ready to store and validate user data securely.
5. Auth Routes (Register & Login)
In this section, we’ll:
- Create routes for user registration and login
- Use bcryptjs to hash passwords
- Use jsonwebtoken to generate a secure token
Step 1: Create routes/auth.js
In routes/auth.js, add the following code:
import express from 'express';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import User from '../models/User.js';
const router = express.Router();
// @route POST /api/auth/register
// @desc Register new user
// @access Public
router.post('/register', async (req, res) => {
const { name, email, password } = req.body;
try {
// Check if user already exists
let user = await User.findOne({ email });
if (user) return res.status(400).json({ message: 'User already exists' });
// Hash password
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(password, salt);
// Create new user
user = new User({ name, email, password: hashedPassword });
await user.save();
// Generate JWT
const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET, {
expiresIn: '1h'
});
res.status(201).json({
token,
user: { id: user._id, name: user.name, email: user.email }
});
} catch (err) {
console.error(err.message);
res.status(500).send('Server error');
}
});
// @route POST /api/auth/login
// @desc Authenticate user & get token
// @access Public
router.post('/login', async (req, res) => {
const { email, password } = req.body;
try {
// Check for user
const user = await User.findOne({ email });
if (!user) return res.status(400).json({ message: 'Invalid credentials' });
// Compare password
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) return res.status(400).json({ message: 'Invalid credentials' });
// Generate JWT
const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET, {
expiresIn: '1h'
});
res.json({
token,
user: { id: user._id, name: user.name, email: user.email }
});
} catch (err) {
console.error(err.message);
res.status(500).send('Server error');
}
});
export default router;
Step 2: Add the Route to server.js
In server.js, make sure you’ve already added:
import authRoutes from './routes/auth.js';
app.use('/api/auth', authRoutes);
Now your authentication routes are live at:
- POST /api/auth/register
- POST /api/auth/login
Quick Test with Postman
Register:
- Method: POST
- URL: http://localhost:3000/api/auth/register
- Body (JSON):
{
"name": "John Doe",
"email": "[email protected]",
"password": "123456"
}
Login:
- Method: POST
- URL: http://localhost:3000/api/auth/login
- Body (JSON):
{
"email": "[email protected]",
"password": "123456"
}
You're now able to securely register and log in users using hashed passwords and JWT.
6. JWT Middleware for Protected Routes
Now that users can register and log in, we need to protect routes so that only authenticated users with a valid token can access them.
We’ll do this by creating a custom Express middleware that:
- Checks for the presence of a JWT in the Authorization header.
- Verifies the token’s validity.
- Adds the decoded user info to the req object.
1. Create a Middleware File
middleware/auth.js:
import jwt from "jsonwebtoken";
const auth = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith("Bearer "))
return res.status(401).json({ message: "No token, authorization denied" });
const token = authHeader.split(" ")[1];
try {
// Verify and decode the token
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// decoded is { id: user._id, iat: ..., exp: ... }
req.user = decoded; // ← assign the whole decoded payload
next();
} catch (err) {
res.status(401).json({ message: "Token is not valid" });
}
};
export default auth;
2. Create a Protected Route to Test It
Create a new route file:
routes/protected.js
import express from 'express';
import auth from '../middleware/auth.js';
const router = express.Router();
router.get("/dashboard", auth, (req, res) => {
res.json({
message: `Welcome, user with ID ${req.user.id}`
});
});
export default router;
3. Register This Route in server.js
In your main server.js, import and use it:
import protectedRoutes from './routes/protected.js';
app.use('/api', protectedRoutes);
4. Test with CURL or Postman
- First, log in to get the JWT token:
POST http://localhost:3000/api/auth/login
Content-Type: application/json
{
"email": "[email protected]",
"password": "123456"
}
Copy the token from the response.
- Then call the protected route:
curl --location 'http://localhost:3000/api/dashboard' \
--header 'Authorization: Bearer YOUR_TOKEN_HERE'
If the token is valid, you'll get:
{
"message": "Welcome, user with ID 123abc..."
}
If the token is missing or invalid:
{
"message": "No token, authorization denied"
}
7. Create Protected CRUD Routes (Example: Posts)
In this section, we'll:
- Create a Mongoose model for a Post
- Set up protected routes for:
- Creating a post
- Reading all posts
- Updating a post
- Deleting a post
- 3. Use the auth middleware to restrict access
1. Create the Post Model
models/Post.js
import mongoose from 'mongoose';
const PostSchema = new mongoose.Schema(
{
title: {
type: String,
required: true,
},
content: {
type: String,
required: true,
},
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true,
},
},
{ timestamps: true }
);
export default mongoose.model('Post', PostSchema);
2. Create the Post Routes
routes/posts.js
import express from 'express';
import Post from '../models/Post.js';
import auth from '../middleware/auth.js';
const router = express.Router();
// Create a new post
router.post('/', auth, async (req, res) => {
try {
const newPost = new Post({
title: req.body.title,
content: req.body.content,
user: req.user.id,
});
const savedPost = await newPost.save();
res.status(201).json(savedPost);
} catch (err) {
res.status(500).json({ message: err.message });
}
});
// Get all posts (for the authenticated user)
router.get('/', auth, async (req, res) => {
try {
const posts = await Post.find({ user: req.user.id }).sort({ createdAt: -1 });
res.json(posts);
} catch (err) {
res.status(500).json({ message: err.message });
}
});
// Update a post
router.put('/:id', auth, async (req, res) => {
try {
const post = await Post.findOne({ _id: req.params.id, user: req.user.id });
if (!post) return res.status(404).json({ message: 'Post not found' });
post.title = req.body.title || post.title;
post.content = req.body.content || post.content;
const updatedPost = await post.save();
res.json(updatedPost);
} catch (err) {
res.status(500).json({ message: err.message });
}
});
// Delete a post
router.delete('/:id', auth, async (req, res) => {
try {
const post = await Post.findOneAndDelete({ _id: req.params.id, user: req.user.id });
if (!post) return res.status(404).json({ message: 'Post not found' });
res.json({ message: 'Post deleted successfully' });
} catch (err) {
res.status(500).json({ message: err.message });
}
});
export default router;
3. Register the Route in server.js
Update your server.js:
import postRoutes from './routes/posts.js';
app.use('/api/posts', postRoutes);
4. Test the CRUD Endpoints
Use Postman or curl with your JWT token to test:
Create Post
curl --location 'http://localhost:3000/api/posts' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_AUTH_TOKEN' \
--data '{
"title": "First Post",
"content": "This is a secure post!"
}'
Get Posts
curl --location 'http://localhost:3000/api/posts' \
--header 'Authorization: Bearer YOUR_AUTH_TOKEN'
Update Post
curl --location --request PUT 'http://localhost:3000/api/posts/6826a37c71519b349ab6f19b' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_AUTH_TOKEN' \
--data '{
"title": "Updated Post Title"
}'
Delete Post
curl --location --request DELETE 'http://localhost:3000/api/posts/6826a37c71519b349ab6f19b' \
--header 'Authorization: Bearer YOUR_AUTH_TOKEN'
That’s a complete protected CRUD setup!
8. User Role Authorization (Admin vs User)
1. Add a role field to the User Model
Update models/User.js:
const UserSchema = new mongoose.Schema(
{
name: { type: String, required: true, trim: true },
email: { type: String, required: true, unique: true, lowercase: true },
password: { type: String, required: true },
role: { type: String, enum: ["user", "admin"], default: "user" }
},
{ timestamps: true }
);
2. Modify JWT Payload to Include Role
In your routes/auth.js, inside the login route:
const token = jwt.sign(
{ id: user._id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: "1h" }
);
3. Create Admin Middleware
middleware/admin.js
const admin = (req, res, next) => {
if (req.user.role !== 'admin') {
return res.status(403).json({ message: 'Access denied: Admins only' });
}
next();
};
export default admin;
4. Use Role Middleware in a Route
In routes/posts.js or create a new route like routes/admin.js:
import express from "express";
import admin from "../middleware/admin.js";
const router = express.Router();
router.get("/admin-only", auth, admin, (req, res) => {
res.json({ message: "Welcome, Admin!" });
});
export default router;
Now, only users with the role "admin" can access this route.
9. Input Validation with Joi
We’ll use Joi for clean, schema-based validation.
1. Install Joi
npm install --legacy-peer-deps joi
2. Create Validation Schemas
Create validators/auth.js
import Joi from 'joi';
export const registerSchema = Joi.object({
name: Joi.string().min(2).max(30).required(),
email: Joi.string().email().required(),
password: Joi.string().min(6).required(),
});
export const loginSchema = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().required(),
});
3. Validate Input in Routes
In routes/auth.js:
import { registerSchema, loginSchema } from '../validators/auth.js';
router.post('/register', async (req, res) => {
const { error } = registerSchema.validate(req.body);
if (error) return res.status(400).json({ message: error.details[0].message });
// continue with registration...
});
Same for login:
router.post('/login', async (req, res) => {
const { error } = loginSchema.validate(req.body);
if (error) return res.status(400).json({ message: error.details[0].message });
// continue with login...
});
Optional: Zod Instead of Joi?
If you prefer Zod:
npm install zod
Then you can define and parse like:
import { z } from 'zod';
const registerSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
password: z.string().min(6),
});
try {
registerSchema.parse(req.body);
} catch (err) {
return res.status(400).json({ message: err.errors[0].message });
}
Both Joi and Zod are great — Joi is more common in Node.js/Express setups.
10. Conclusion
In this tutorial, you learned how to build a secure and modern REST API using Node.js, Express, MongoDB, and JWT authentication. We walked through every essential step—from project setup, user registration, and login, to protecting routes using tokens.
You also enhanced your API by:
- Implementing role-based authorization to control access for different users (e.g., admin vs. user)
- Adding data validation with Joi (or Zod) to ensure safe and clean user input
- Creating protected CRUD routes with real-world examples, like managing posts
This structure provides a strong foundation for building scalable and secure backends. You can easily extend it further by adding:
- Password reset with email
- Refresh tokens
- Rate limiting & security headers
- Logging and monitoring
With this backend setup, you're ready to connect it to any frontend framework like React, Angular, Vue, or a mobile app built with Flutter or React Native.
You can find the full source code on our GitHub Repository.
That just the basic. If you need more deep learning about Node.js, Express.js, PostgreSQL, Vue.js and GraphQL or related you can take the following cheap course:
- Node. JS
- Learning Express JS: Creating Future Web Apps & REST APIs
- Angular + NodeJS + PostgreSQL + Sequelize
- Build 7 Real World Applications with Vue. js
- Full-Stack Vue with GraphQL - The Ultimate Guide
Thanks!