Express User Management
User management is a critical component of most web applications. It includes functionalities such as user registration, authentication, profile management, and access control. In this guide, we'll learn how to implement a comprehensive user management system in an Express.js application.
Introduction to User Management
User management involves handling various aspects of user accounts throughout their lifecycle in your application:
- User registration and account creation
- User authentication (login and logout)
- Password management (reset, change)
- Profile management
- Role-based access control
- Account deletion
Implementing these features correctly ensures security, good user experience, and proper data management in your application.
Prerequisites
Before starting, ensure you have:
- Basic understanding of Express.js
- Node.js installed on your machine
- MongoDB or another database system
- Understanding of JWT (JSON Web Tokens) or session-based authentication
Setting Up Your Project
First, let's create a new Express project with the necessary dependencies:
mkdir express-user-management
cd express-user-management
npm init -y
npm install express mongoose bcryptjs jsonwebtoken validator dotenv cookie-parser
npm install nodemon --save-dev
Create the project structure:
express-user-management/
├── config/
│ └── db.js
├── controllers/
│ └── userController.js
├── middleware/
│ └── auth.js
├── models/
│ └── User.js
├── routes/
│ └── userRoutes.js
├── .env
├── app.js
└── package.json
Creating the User Model
Let's start by defining our User model using Mongoose:
// models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const validator = require('validator');
const userSchema = new mongoose.Schema({
name: {
type: String,
required: [true, 'Please provide a name'],
trim: true,
maxlength: [50, 'Name cannot exceed 50 characters']
},
email: {
type: String,
required: [true, 'Please provide an email'],
unique: true,
lowercase: true,
validate: [validator.isEmail, 'Please provide a valid email']
},
password: {
type: String,
required: [true, 'Please provide a password'],
minlength: [8, 'Password must be at least 8 characters'],
select: false
},
role: {
type: String,
enum: ['user', 'admin'],
default: 'user'
},
createdAt: {
type: Date,
default: Date.now
}
});
// Hash password before saving
userSchema.pre('save', async function(next) {
// Only run if password is modified
if (!this.isModified('password')) return next();
// Hash password with strength of 12
this.password = await bcrypt.hash(this.password, 12);
next();
});
// Method to check if password matches
userSchema.methods.correctPassword = async function(candidatePassword, userPassword) {
return await bcrypt.compare(candidatePassword, userPassword);
};
// Method to generate JWT token
userSchema.methods.generateAuthToken = function() {
return jwt.sign({ id: this._id }, process.env.JWT_SECRET, {
expiresIn: process.env.JWT_EXPIRES_IN
});
};
const User = mongoose.model('User', userSchema);
module.exports = User;
Database Connection
Let's set up the database connection:
// config/db.js
const mongoose = require('mongoose');
const connectDB = async () => {
try {
const conn = await mongoose.connect(process.env.MONGO_URI, {
useNewUrlParser: true,
useUnifiedTopology: true
});
console.log(`MongoDB Connected: ${conn.connection.host}`);
} catch (error) {
console.error(`Error: ${error.message}`);
process.exit(1);
}
};
module.exports = connectDB;
Authentication Middleware
Create middleware to protect routes:
// middleware/auth.js
const jwt = require('jsonwebtoken');
const User = require('../models/User');
exports.protect = async (req, res, next) => {
try {
// 1. Get token from header
const token = req.header('Authorization')?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ message: 'Please log in to access this resource' });
}
// 2. Verify token
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// 3. Check if user still exists
const currentUser = await User.findById(decoded.id);
if (!currentUser) {
return res.status(401).json({ message: 'The user no longer exists' });
}
// 4. Grant access to protected route
req.user = currentUser;
next();
} catch (error) {
res.status(401).json({ message: 'Please authenticate' });
}
};
exports.restrictTo = (...roles) => {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({
message: 'You do not have permission to perform this action'
});
}
next();
};
};
User Controller
Now, let's implement the user controller with all necessary functionalities:
// controllers/userController.js
const User = require('../models/User');
// @desc Register a new user
// @route POST /api/users/register
// @access Public
exports.registerUser = async (req, res) => {
try {
const { name, email, password } = req.body;
// Check if user already exists
const userExists = await User.findOne({ email });
if (userExists) {
return res.status(400).json({ message: 'User already exists' });
}
// Create new user
const user = await User.create({
name,
email,
password
});
// Generate token
const token = user.generateAuthToken();
res.status(201).json({
success: true,
data: {
id: user._id,
name: user.name,
email: user.email,
role: user.role
},
token
});
} catch (error) {
res.status(500).json({
message: 'Error in creating user',
error: error.message
});
}
};
// @desc Login user
// @route POST /api/users/login
// @access Public
exports.loginUser = async (req, res) => {
try {
const { email, password } = req.body;
// Check if email and password are provided
if (!email || !password) {
return res.status(400).json({ message: 'Please provide email and password' });
}
// Find user by email and include password for comparison
const user = await User.findOne({ email }).select('+password');
// Check if user exists and password matches
if (!user || !(await user.correctPassword(password, user.password))) {
return res.status(401).json({ message: 'Invalid email or password' });
}
// Generate token
const token = user.generateAuthToken();
res.status(200).json({
success: true,
data: {
id: user._id,
name: user.name,
email: user.email,
role: user.role
},
token
});
} catch (error) {
res.status(500).json({
message: 'Login error',
error: error.message
});
}
};
// @desc Get current user profile
// @route GET /api/users/profile
// @access Private
exports.getUserProfile = async (req, res) => {
try {
const user = await User.findById(req.user.id);
res.status(200).json({
success: true,
data: {
id: user._id,
name: user.name,
email: user.email,
role: user.role,
createdAt: user.createdAt
}
});
} catch (error) {
res.status(500).json({
message: 'Error fetching profile',
error: error.message
});
}
};
// @desc Update user profile
// @route PUT /api/users/profile
// @access Private
exports.updateUserProfile = async (req, res) => {
try {
const { name, email } = req.body;
// Find user and update
const user = await User.findByIdAndUpdate(
req.user.id,
{ name, email },
{ new: true, runValidators: true }
);
res.status(200).json({
success: true,
data: {
id: user._id,
name: user.name,
email: user.email,
role: user.role
}
});
} catch (error) {
res.status(500).json({
message: 'Error updating profile',
error: error.message
});
}
};
// @desc Change password
// @route PUT /api/users/change-password
// @access Private
exports.changePassword = async (req, res) => {
try {
const { currentPassword, newPassword } = req.body;
// Get user with password
const user = await User.findById(req.user.id).select('+password');
// Check if current password matches
if (!(await user.correctPassword(currentPassword, user.password))) {
return res.status(401).json({ message: 'Current password is incorrect' });
}
// Update password
user.password = newPassword;
await user.save();
// Generate new token
const token = user.generateAuthToken();
res.status(200).json({
success: true,
message: 'Password updated successfully',
token
});
} catch (error) {
res.status(500).json({
message: 'Error changing password',
error: error.message
});
}
};
// @desc Get all users (admin only)
// @route GET /api/users
// @access Private/Admin
exports.getUsers = async (req, res) => {
try {
const users = await User.find().select('-password');
res.status(200).json({
success: true,
count: users.length,
data: users
});
} catch (error) {
res.status(500).json({
message: 'Error fetching users',
error: error.message
});
}
};
// @desc Delete user (admin only)
// @route DELETE /api/users/:id
// @access Private/Admin
exports.deleteUser = async (req, res) => {
try {
const user = await User.findByIdAndDelete(req.params.id);
if (!user) {
return res.status(404).json({ message: 'User not found' });
}
res.status(200).json({
success: true,
message: 'User deleted successfully'
});
} catch (error) {
res.status(500).json({
message: 'Error deleting user',
error: error.message
});
}
};
Setting Up Routes
Let's define our user routes:
// routes/userRoutes.js
const express = require('express');
const router = express.Router();
const userController = require('../controllers/userController');
const { protect, restrictTo } = require('../middleware/auth');
// Public routes
router.post('/register', userController.registerUser);
router.post('/login', userController.loginUser);
// Protected routes
router.get('/profile', protect, userController.getUserProfile);
router.put('/profile', protect, userController.updateUserProfile);
router.put('/change-password', protect, userController.changePassword);
// Admin routes
router.get('/', protect, restrictTo('admin'), userController.getUsers);
router.delete('/:id', protect, restrictTo('admin'), userController.deleteUser);
module.exports = router;
Main Application File
Finally, let's set up our main application file:
// app.js
const express = require('express');
const dotenv = require('dotenv');
const connectDB = require('./config/db');
const userRoutes = require('./routes/userRoutes');
const cookieParser = require('cookie-parser');
// Load environment variables
dotenv.config();
// Connect to database
connectDB();
const app = express();
// Middleware
app.use(express.json());
app.use(cookieParser());
// Routes
app.use('/api/users', userRoutes);
// Basic home route
app.get('/', (req, res) => {
res.send('API is running');
});
// Error handling middleware
app.use((err, req, res, next) => {
const statusCode = res.statusCode === 200 ? 500 : res.statusCode;
res.status(statusCode);
res.json({
message: err.message,
stack: process.env.NODE_ENV === 'production' ? null : err.stack
});
});
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
Environment Variables
Create a .env
file in your project root:
PORT=5000
NODE_ENV=development
MONGO_URI=mongodb://localhost:27017/user-management
JWT_SECRET=your_jwt_secret_key
JWT_EXPIRES_IN=30d
Testing the User Management System
Once your application is set up, you can test the API endpoints using Postman or any other API testing tool:
Register a new user:
Request:
POST /api/users/register
Content-Type: application/json
{
"name": "John Doe",
"email": "[email protected]",
"password": "password123"
}
Response:
{
"success": true,
"data": {
"id": "60c72b2b8f40e123456789ab",
"name": "John Doe",
"email": "[email protected]",
"role": "user"
},
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Login:
Request:
POST /api/users/login
Content-Type: application/json
{
"email": "[email protected]",
"password": "password123"
}
Response:
{
"success": true,
"data": {
"id": "60c72b2b8f40e123456789ab",
"name": "John Doe",
"email": "[email protected]",
"role": "user"
},
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Get user profile (with Authorization header):
Request:
GET /api/users/profile
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Response:
{
"success": true,
"data": {
"id": "60c72b2b8f40e123456789ab",
"name": "John Doe",
"email": "[email protected]",
"role": "user",
"createdAt": "2023-06-14T10:23:23.789Z"
}
}
Real-World Applications
User management systems are integral to many applications. Here are some real-world scenarios where this implementation would be useful:
E-commerce Platform
In an e-commerce application, you might extend this system to:
- Store user shipping addresses
- Track order history
- Implement customer roles (regular, premium)
- Save payment methods securely
// Extended user schema for e-commerce
const userSchema = new mongoose.Schema({
// ...existing fields
addresses: [{
street: String,
city: String,
state: String,
zipCode: String,
country: String,
isDefault: Boolean
}],
paymentMethods: [{
type: { type: String, enum: ['credit', 'debit', 'paypal'] },
lastFour: String,
provider: String,
isDefault: Boolean
}],
preferences: {
newsletter: { type: Boolean, default: true },
productRecommendations: { type: Boolean, default: true }
}
});
Content Management System
For a CMS, you might need more granular user roles:
// CMS role-based access
const userSchema = new mongoose.Schema({
// ...existing fields
role: {
type: String,
enum: ['reader', 'contributor', 'editor', 'admin'],
default: 'reader'
},
permissions: [{
type: String,
enum: ['read', 'create', 'update', 'delete', 'publish']
}]
});
// Middleware to check specific permissions
exports.hasPermission = (permission) => {
return (req, res, next) => {
if (!req.user.permissions.includes(permission)) {
return res.status(403).json({ message: 'You do not have permission to perform this action' });
}
next();
};
};
Best Practices for User Management
-
Password Security:
- Always hash passwords before storing
- Implement password complexity requirements
- Offer secure password reset options
-
Data Protection:
- Only return necessary user data (avoid sensitive data)
- Use HTTPS for all communications
- Implement rate limiting for authentication attempts
-
Token Management:
- Set reasonable expiration times for tokens
- Implement token refresh mechanisms
- Consider token revocation strategies
-
User Experience:
- Provide clear feedback for registration/login errors
- Implement email verification for new accounts
- Create intuitive profile management interfaces
Summary
In this guide, we've created a comprehensive user management system for Express.js applications, including:
- User registration and authentication
- Secure password handling
- Profile management functionality
- Role-based access control
- Administrative capabilities
This foundation can be extended to fit the specific needs of your application, whether it's an e-commerce platform, content management system, social network, or any other system requiring user accounts.
Additional Resources and Exercises
Resources
- Express.js Documentation
- Mongoose Documentation
- JWT Introduction
- OWASP Authentication Best Practices
Exercises
-
Password Reset Flow: Implement a complete password reset flow with email verification.
-
Two-Factor Authentication: Add 2FA to your authentication system using libraries like
speakeasy
. -
OAuth Integration: Extend the system to allow login with Google, Facebook, or GitHub.
-
Account Verification: Implement email verification for newly registered accounts.
-
User Activity Logging: Create middleware to log user activities and login attempts.
By mastering user management in Express.js, you'll be able to create secure, user-friendly applications that properly handle user data and authentication needs.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)