Welcome back, AnakInformatika! In today’s digital landscape, building modern web applications almost always involves using a REST API as the bridge between the frontend and backend. However, functionality alone isn't enough; security is a non-negotiable foundation.
Imagine building an app that handles sensitive user data. Without proper safeguards, that data is vulnerable to unauthorized access. In this tutorial, we will walk you through a detailed guide on How to Create a Secure REST API using Express.js and JWT (JSON Web Tokens)—a powerful combination for robust authentication and authorization.
We will cover everything from project setup and secure registration with bcrypt password hashing to implementing JWT to protect your API routes. Ready to build an API that is both functional and super secure? Let’s dive in!
Prerequisites
Before we jump into the code, ensure you have the following:
-
Node.js & npm: Installed on your system (Download from Node.js official site).
-
Basic JavaScript & Express.js Knowledge: Familiarity with ES6 syntax and Express concepts like routing and middleware.
-
Code Editor: Visual Studio Code is highly recommended.
-
MongoDB: Local installation or a free MongoDB Atlas account.
Step 1: Project Initialization and Dependencies
First, let's set up the project folder and install the necessary libraries.
1.1 Create a New Project Folder
Open your terminal and run:
mkdir secure-api-jwt
cd secure-api-jwt
1.2 Initialize Node.js
npm init -y
1.3 Install Dependencies
npm install express jsonwebtoken bcryptjs dotenv mongoose
-
express: Web framework for Node.js.
-
jsonwebtoken: To generate and verify JWTs.
-
bcryptjs: To hash passwords securely.
-
dotenv: To manage environment variables.
-
mongoose: ODM (Object Data Modeling) for MongoDB.
Step 2: Configure Environment Variables (.env)
Never hardcode sensitive data like JWT secrets or database URIs. Create a .env file in your root directory:
PORT=5000
JWT_SECRET=your_super_strong_random_secret_key
MONGO_URI=mongodb://localhost:27017/secure_api_db
Note: Replace
your_super_strong_random_secret_keywith a long, complex string.
Step 3: MongoDB Database Connection
Create a config/db.js file to handle the connection logic:
// config/db.js
const mongoose = require('mongoose');
require('dotenv').config();
const connectDB = async () => {
try {
await mongoose.connect(process.env.MONGO_URI);
console.log('MongoDB Connected...');
} catch (err) {
console.error(err.message);
process.exit(1);
}
};
module.exports = connectDB;
Step 4: Define the User Model
We need a schema to define our users and handle password encryption. Create models/User.js:
// models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const UserSchema = new mongoose.Schema({
username: { type: String, required: true, unique: true },
password: { type: String, required: true }
});
// Hash password before saving to database
UserSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
next();
});
// Method to compare passwords
UserSchema.methods.comparePassword = async function(candidatePassword) {
return await bcrypt.compare(candidatePassword, this.password);
};
module.exports = mongoose.model('User', UserSchema);
Step 5: Create JWT Authentication Middleware
This middleware will intercept requests to protected routes to verify the token. Create middleware/auth.js:
// middleware/auth.js
const jwt = require('jsonwebtoken');
require('dotenv').config();
module.exports = function(req, res, next) {
const token = req.header('x-auth-token');
if (!token) {
return res.status(401).json({ msg: 'No token, authorization denied' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded.user;
next();
} catch (err) {
res.status(401).json({ msg: 'Token is not valid' });
}
};
Step 6: Create Authentication Routes (Register & Login)
Create routes/auth.js to handle user registration and login logic. This is where we generate the JWT and send it back to the client.
// routes/auth.js
const express = require('express');
const router = express.Router();
const jwt = require('jsonwebtoken');
const User = require('../models/User');
require('dotenv').config();
// @route POST api/auth/register
// @desc Mendaftarkan user baru
// @access Public
router.post('/register', async (req, res) => {
const { username, password } = req.body;
try {
let user = await User.findOne({ username });
if (user) {
return res.status(400).json({ msg: 'User sudah terdaftar' });
}
user = new User({
username,
password
});
await user.save(); // Password akan di-hash secara otomatis oleh middleware pre('save') di model User
// Buat payload untuk JWT
const payload = {
user: {
id: user.id
}
};
// Buat token
jwt.sign(
payload,
process.env.JWT_SECRET,
{ expiresIn: '1h' }, // Token akan kadaluarsa dalam 1 jam
(err, token) => {
if (err) throw err;
res.json({ token });
}
);
} catch (err) {
console.error(err.message);
res.status(500).send('Server Error');
}
});
// @route POST api/auth/login
// @desc Login user & mendapatkan token
// @access Public
router.post('/login', async (req, res) => {
const { username, password } = req.body;
try {
let user = await User.findOne({ username });
if (!user) {
return res.status(400).json({ msg: 'Kredensial tidak valid' });
}
const isMatch = await user.comparePassword(password);
if (!isMatch) {
return res.status(400).json({ msg: 'Kredensial tidak valid' });
}
// Buat payload untuk JWT
const payload = {
user: {
id: user.id
}
};
// Buat token
jwt.sign(
payload,
process.env.JWT_SECRET,
{ expiresIn: '1h' },
(err, token) => {
if (err) throw err;
res.json({ token });
}
);
} catch (err) {
console.error(err.message);
res.status(500).send('Server Error');
}
});
module.exports = router;
Code Explanation:
1. Registration Flow
-
Request Handling: Receives
usernameandpasswordfrom the request body. -
Duplication Check: Queries the database to see if the username is already taken.
-
Automated Hashing: If unique, it creates a new user. The password is automatically hashed before saving, thanks to the
pre('save')middleware we defined in the User model. -
Token Generation: Generates a JWT using
jwt.sign(). The payload contains only theuser.idto keep the token lightweight and secure. -
Expiration: Sets the token to expire in 1 hour for better security.
2. Login Flow
-
User Verification: Checks if the username exists in the database.
-
Password Comparison: Uses the custom
user.comparePassword()method to verify if the provided password matches the stored hash. -
Authentication: If credentials are valid, it issues a new JWT, allowing the user to access protected routes.
Step 7: Create Protected Routes
Create routes/protected.js to define endpoints that require a valid token to access:
// Example of a private route
router.get('/me', auth, async (req, res) => {
const user = await User.findById(req.user.id).select('-password');
res.json(user);
});
Step 8: Main Server Setup (app.js)
Assemble everything in your main entry point, app.js:
const express = require('express');
const connectDB = require('./config/db');
const app = express();
connectDB();
app.use(express.json());
app.use('/api/auth', require('./routes/auth'));
app.use('/api/protected', require('./routes/protected'));
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
Best Practices for API Security
Building the API is just the start. To make it production-ready, consider these Security Best Practices:
-
JWT Security: Use a strong secret key and set a reasonable expiration time (e.g., 1 hour). Consider implementing Refresh Tokens for better UX.
-
HTTPS/SSL: Always serve your API over HTTPS to prevent "Man-in-the-Middle" attacks.
-
Input Validation: Use libraries like
express-validatorto sanitize and validate user input. -
Rate Limiting: Use
express-rate-limitto prevent brute-force attacks on your login endpoints. -
CORS Configuration: Only allow trusted domains to access your API using the
corsmiddleware.
Conclusion
Congratulations! You’ve successfully implemented a Secure REST API with Express.js and JWT. You’ve learned how to hash passwords, manage sessions with tokens, and protect sensitive data.
Security is a continuous journey—stay updated with the latest threats and keep experimenting. Happy coding, AnakInformatika!