Build a REST API with Authentication and JWT
Introduction
REST APIs are the backbone of modern web applications. In this tutorial, we'll build a production-ready REST API with complete authentication using JSON Web Tokens (JWT).
We'll implement user registration, login, protected routes, role-based access control, and best practices for API security.
What You'll Build
- Complete REST API with Express
- User registration and login
- JWT-based authentication
- Protected routes with middleware
- Role-based access control (RBAC)
- Input validation
What You'll Learn
- REST API design principles
- JWT token handling
- Password hashing with bcrypt
- Express middleware development
- Input validation with Joi
- API security best practices
Project Overview
Our REST API will manage resources with full CRUD operations and authentication.
API Endpoints
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| POST | /api/auth/register | Register new user | No |
| POST | /api/auth/login | Login user | No |
| GET | /api/auth/me | Get current user | Yes |
| GET | /api/users | List all users | Admin |
| GET | /api/products | List products | Yes |
| POST | /api/products | Create product | Admin |
Prerequisites
- Node.js installed - Version 14+
- MongoDB - Local or Atlas
- Postman - For testing
- Basic JavaScript knowledge
Project Setup
Bash
# Create project directory
mkdir rest-api-jwt
cd rest-api-jwt
# Initialize Node.js
npm init -y
# Install dependencies
npm install express mongoose bcryptjs jsonwebtoken cors dotenv joi helmet express-rate-limit
# Install dev dependency
npm install --save-dev nodemon
Project Structure
File Structure
rest-api-jwt/
├── config/
│ └── db.js
├── controllers/
│ ├── authController.js
│ ├── userController.js
│ └── productController.js
├── middleware/
│ ├── auth.js
│ ├── admin.js
│ └── validate.js
├── models/
│ ├── User.js
│ └── Product.js
├── routes/
│ ├── auth.js
│ ├── users.js
│ └── products.js
├── .env
├── server.js
└── package.json
Environment
# .env
PORT=5000
NODE_ENV=development
MONGO_URI=mongodb://localhost:27017/restapi
JWT_SECRET=your_super_secret_jwt_key_change_this
JWT_EXPIRE=30d
BCRYPT_ROUND=10
Database Setup
Let's set up the MongoDB connection.
JavaScript
// config/db.js
const mongoose = require('mongoose');
const connectDB = async () => {
try {
const conn = await mongoose.connect(process.env.MONGO_URI);
console.log(`MongoDB Connected: ${conn.connection.host}`);
} catch (error) {
console.error(`Error: ${error.message}`);
process.exit(1);
}
};
module.exports = connectDB;
Data Models
Let's create the Mongoose models for users and products.
JavaScript
// models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const userSchema = mongoose.Schema({
name: {
type: String,
required: [true, 'Please provide a name'],
maxlength: [50, 'Name cannot be more than 50 characters']
},
email: {
type: String,
required: [true, 'Please provide an email'],
unique: true,
match: [
/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/,
'Please provide a valid email'
]
},
password: {
type: String,
required: [true, 'Please add a password'],
minlength: [6, 'Password must be at least 6 characters'],
select: false
},
role: {
type: String,
enum: ['user', 'admin'],
default: 'user'
},
avatar: {
type: String,
default: ''
},
resetPasswordToken: String,
resetPasswordExpire: Date
}, {
timestamps: true
});
// Encrypt password using bcrypt
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) {
next();
}
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
});
// Sign JWT and return
userSchema.methods.getSignedJwtToken = function() {
return jwt.sign({ id: this._id }, process.env.JWT_SECRET, {
expiresIn: process.env.JWT_EXPIRE
});
};
// Match password
userSchema.methods.matchPassword = async function(enteredPassword) {
return await bcrypt.compare(enteredPassword, this.password);
};
// Generate reset password token
userSchema.methods.getResetPasswordToken = function() {
const crypto = require('crypto');
// Generate token
const resetToken = crypto.randomBytes(20).toString('hex');
// Hash token and set to resetPasswordToken field
this.resetPasswordToken = crypto
.createHash('sha256')
.update(resetToken)
.digest('hex');
// Set expire
this.resetPasswordExpire = Date.now() + 10 * 60 * 1000;
return resetToken;
};
module.exports = mongoose.model('User', userSchema);
JavaScript
// models/Product.js
const mongoose = require('mongoose');
const productSchema = mongoose.Schema({
user: {
type: mongoose.Schema.ObjectId,
required: true,
ref: 'User'
},
name: {
type: String,
required: [true, 'Please add a product name'],
trim: true
},
description: {
type: String,
required: [true, 'Please add a description']
},
price: {
type: Number,
required: [true, 'Please add a price'],
min: 0
},
category: {
type: String,
required: [true, 'Please add a category'],
enum: ['electronics', 'clothing', 'books', 'other']
},
stock: {
type: Number,
required: true,
min: 0,
default: 0
},
image: {
type: String,
default: ''
}
}, {
timestamps: true
});
module.exports = mongoose.model('Product', productSchema);
Authentication Middleware
Let's create the middleware to protect routes and check roles.
JavaScript
// middleware/auth.js
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const protect = async (req, res, next) => {
let token;
// Check for bearer token in header
if (req.headers.authorization &&
req.headers.authorization.startsWith('Bearer')) {
token = req.headers.authorization.split(' ')[1];
}
// Make sure token exists
if (!token) {
return res.status(401).json({
success: false,
error: 'Not authorized to access this route'
});
}
try {
// Verify token
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Get user from token
req.user = await User.findById(decoded.id);
if (!req.user) {
return res.status(404).json({
success: false,
error: 'No user found with this id'
});
}
next();
} catch (err) {
return res.status(401).json({
success: false,
error: 'Not authorized to access this route'
});
}
};
module.exports = { protect };
JavaScript
// middleware/admin.js
const protect = require('./auth');
const admin = (req, res, next) => {
if (req.user && req.user.role === 'admin') {
next();
} else {
return res.status(403).json({
success: false,
error: 'Not authorized as an admin'
});
}
};
module.exports = { protect, admin };
JavaScript
// middleware/validate.js
const Joi = require('joi');
const validate = (schema) => {
return (req, res, next) => {
const { error } = schema.validate(req.body);
if (error) {
const firstError = error.details.map(detail => detail.message).join(', ');
return res.status(400).json({
success: false,
error: firstError
});
}
next();
};
};
// Validation schemas
const schemas = {
register: Joi.object({
name: Joi.string().max(50).required(),
email: Joi.string().email().required(),
password: Joi.string().min(6).required()
}),
login: Joi.object({
email: Joi.string().email().required(),
password: Joi.string().required()
}),
product: Joi.object({
name: Joi.string().required(),
description: Joi.string().required(),
price: Joi.number().min(0).required(),
category: Joi.string().valid('electronics', 'clothing', 'books', 'other').required(),
stock: Joi.number().min(0).default(0)
})
};
module.exports = { validate, schemas };
Security Best Practices
- Always use HTTPS in production
- Store JWT secret in environment variables
- Set appropriate token expiration
- Use HTTP-only cookies for sensitive apps
API Routes
Let's create the route handlers for authentication and resources.
JavaScript
// routes/auth.js
const express = require('express');
const router = express.Router();
const { protect } = require('../middleware/auth');
const { validate, schemas } = require('../middleware/validate');
const User = require('../models/User';
// @route POST /api/auth/register
// @desc Register user
// @access Public
router.post('/register', validate(schemas.register), async (req, res) => {
try {
const { name, email, password } = req.body;
// Check if user exists
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(400).json({
success: false,
error: 'Email already exists'
});
}
// Create user
const user = await User.create({ name, email, password });
sendTokenResponse(user, 201, res);
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
});
// @route POST /api/auth/login
// @desc Login user
// @access Public
router.post('/login', validate(schemas.login), async (req, res) => {
try {
const { email, password } = req.body;
// Check for user
const user = await User.findOne({ email }).select('+password');
if (!user) {
return res.status(401).json({
success: false,
error: 'Invalid credentials'
});
}
// Check if password matches
const isMatch = await user.matchPassword(password);
if (!isMatch) {
return res.status(401).json({
success: false,
error: 'Invalid credentials'
});
}
sendTokenResponse(user, 200, res);
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
});
// @route GET /api/auth/me
// @desc Get current logged in user
// @access Private
router.get('/me', protect, async (req, res) => {
const user = await User.findById(req.user.id);
res.status(200).json({
success: true,
data: user
});
});
// Helper function to get token and send response
const sendTokenResponse = (user, statusCode, res) => {
const token = user.getSignedJwtToken();
const options = {
expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
httpOnly: true
};
if (process.env.NODE_ENV === 'production') {
options.secure = true;
}
res
.status(statusCode)
.cookie('token', token, options)
.json({
success: true,
token,
data: {
id: user._id,
name: user.name,
email: user.email,
role: user.role
}
});
};
module.exports = router;
JavaScript
// routes/products.js
const express = require('express');
const router = express.Router();
const { protect, admin } = require('../middleware/admin');
const { validate, schemas } = require('../middleware/validate');
const Product = require('../models/Product';
// @route GET /api/products
// @desc Get all products
// @access Private
router.get('/', protect, async (req, res) => {
try {
const products = await Product.find();
res.status(200).json({
success: true,
count: products.length,
data: products
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
});
// @route GET /api/products/:id
// @desc Get single product
// @access Private
router.get('/:id', protect, async (req, res) => {
try {
const product = await Product.findById(req.params.id);
if (!product) {
return res.status(404).json({
success: false,
error: 'Product not found'
});
}
res.status(200).json({
success: true,
data: product
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
});
// @route POST /api/products
// @desc Create product
// @access Private (Admin only)
router.post('/', [protect, admin, validate(schemas.product)], async (req, res) => {
try {
const product = await Product.create({
...req.body,
user: req.user.id
});
res.status(201).json({
success: true,
data: product
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
});
// @route PUT /api/products/:id
// @desc Update product
// @access Private (Admin only)
router.put('/:id', [protect, admin], async (req, res) => {
try {
let product = await Product.findById(req.params.id);
if (!product) {
return res.status(404).json({
success: false,
error: 'Product not found'
});
}
product = await Product.findByIdAndUpdate(req.params.id, req.body, {
new: true,
runValidators: true
});
res.status(200).json({
success: true,
data: product
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
});
// @route DELETE /api/products/:id
// @desc Delete product
// @access Private (Admin only)
router.delete('/:id', [protect, admin], async (req, res) => {
try {
const product = await Product.findById(req.params.id);
if (!product) {
return res.status(404).json({
success: false,
error: 'Product not found'
});
}
await product.deleteOne();
res.status(200).json({
success: true,
data: {}
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
});
module.exports = router;
Main Server File
Let's put it all together in the main server file.
JavaScript
// server.js
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const connectDB = require('./config/db');
require('dotenv').config();
// Connect to database
connectDB();
const app = express();
// Body parser
app.use(express.json());
// Security middleware
app.use(helmet());
app.use(cors());
// Rate limiting
const limiter = rateLimit({
windowMs: 10 * 60 * 1000,
max: 100
});
app.use(limiter);
// Mount routes
app.use('/api/auth', require('./routes/auth'));
app.use('/api/users', require('./routes/users'));
app.use('/api/products', require('./routes/products'));
// Root route
app.get('/', (req, res) => {
res.json({ message: 'Welcome to the API' });
});
const PORT = process.env.PORT || 5000;
const server = app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
// Handle unhandled promise rejections
process.on('unhandledRejection', (err, promise) => {
console.log(`Error: ${err.message}`);
server.close(() => process.exit(1));
});
Testing the API
Bash
# Start the server
npm run dev
# Test endpoints with curl
# 1. Register a user
curl -X POST http://localhost:5000/api/auth/register \
-H "Content-Type: application/json" \
-d '{
"name": "John Doe",
"email": "john@example.com",
"password": "password123"
}'
# 2. Login
curl -X POST http://localhost:5000/api/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "john@example.com",
"password": "password123"
}'
# 3. Get current user (with token)
curl http://localhost:5000/api/auth/me \
-H "Authorization: Bearer YOUR_TOKEN"
# 4. Create a product (as admin)
curl -X POST http://localhost:5000/api/products \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
-d '{
"name": "Laptop",
"description": "A powerful laptop",
"price": 999,
"category": "electronics",
"stock": 10
}'
Testing Checklist
- ✓ User can register
- ✓ User can login and receive JWT
- ✓ Protected routes require token
- ✓ Admin routes require admin role
- ✓ Input validation works
- ✓ CRUD operations work
Summary
Congratulations! You've built a complete REST API with authentication.
What You Built
- REST API - Full CRUD operations
- JWT Authentication - Secure login and registration
- Protected Routes - Middleware for authorization
- Role-Based Access - Admin and user roles
- Input Validation - Joi validation
- Security - Helmet, rate limiting
Next Steps
- Add refresh tokens
- Implement password reset
- Add email verification
- Add pagination and filtering