Skip to main content

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:

bash
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:

javascript
// 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:

javascript
// 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:

javascript
// 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:

javascript
// 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:

javascript
// 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:

javascript
// 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:

json
{
"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:

json
{
"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:

json
{
"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
javascript
// 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:

javascript
// 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

  1. Password Security:

    • Always hash passwords before storing
    • Implement password complexity requirements
    • Offer secure password reset options
  2. Data Protection:

    • Only return necessary user data (avoid sensitive data)
    • Use HTTPS for all communications
    • Implement rate limiting for authentication attempts
  3. Token Management:

    • Set reasonable expiration times for tokens
    • Implement token refresh mechanisms
    • Consider token revocation strategies
  4. 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

Exercises

  1. Password Reset Flow: Implement a complete password reset flow with email verification.

  2. Two-Factor Authentication: Add 2FA to your authentication system using libraries like speakeasy.

  3. OAuth Integration: Extend the system to allow login with Google, Facebook, or GitHub.

  4. Account Verification: Implement email verification for newly registered accounts.

  5. 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! :)