Building Secure User Registration APIs with Node.js, Express, and MongoDB: A Comprehensive Guide

alternative
alternative

Hanzala
13 min read  ⋅ Sept 14, 2024

In today’s digital landscape, creating robust and secure user registration systems is crucial for web applications. This guide will walk you through building CRUD (Create, Read, Update, Delete) APIs for user registration with authentication using Node.js, Express.js, and MongoDB. We’ll focus on best practices and security measures to ensure your application is production-ready.

Table of Contents

  1. Setting Up the Project
  2. Connecting to MongoDB
  3. Creating the User Model
  4. Implementing User Registration
  5. User Authentication
  6. CRUD Operations
  7. Security Best Practices
  8. Testing the APIs
  9. Conclusion

Setting Up the Project

First, let’s set up our Node.js project and install the necessary dependencies.

bash
mkdir user-registration-api
cd user-registration-api
npm init -y
npm install express mongoose bcryptjs jsonwebtoken dotenv
npm install --save-dev nodemon

Create a server.js file in the root directory:

javascript
const express = require('express');
const mongoose = require('mongoose');
const dotenv = require('dotenv');

dotenv.config();

const app = express();

app.use(express.json());

const PORT = process.env.PORT || 3000;

app.listen(PORT, () => console.log(`Server running on port ${PORT}`));

Connecting to MongoDB

Create a .env file in the root directory to store your MongoDB connection string:

MONGODB_URI=mongodb://localhost:27017/user_registration
JWT_SECRET=your_jwt_secret

Update server.js to include the MongoDB connection:

javascript
mongoose.connect(process.env.MONGODB_URI, {
  useNewUrlParser: true,
  useUnifiedTopology: true,
})
.then(() => console.log('Connected to MongoDB'))
.catch((err) => console.error('MongoDB connection error:', err));

Creating the User Model

Create a models folder and add a User.js file:

javascript
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');

const userSchema = new mongoose.Schema({
  username: { type: String, required: true, unique: true },
  email: { type: String, required: true, unique: true },
  password: { type: String, required: true },
  createdAt: { type: Date, default: Date.now },
});

userSchema.pre('save', async function(next) {
  if (!this.isModified('password')) return next();
  this.password = await bcrypt.hash(this.password, 12);
  next();
});

userSchema.methods.comparePassword = async function(candidatePassword) {
  return await bcrypt.compare(candidatePassword, this.password);
};

module.exports = mongoose.model('User', userSchema);

Implementing User Registration

Create a routes folder and add a auth.js file:

javascript
const express = require('express');
const User = require('../models/User');
const jwt = require('jsonwebtoken');

const router = express.Router();

router.post('/register', async (req, res) => {
  try {
    const { username, email, password } = req.body;

    const existingUser = await User.findOne({ $or: [{ email }, { username }] });
    if (existingUser) {
      return res.status(400).json({ message: 'User already exists' });
    }

    const user = new User({ username, email, password });
    await user.save();

    res.status(201).json({ message: 'User registered successfully' });
  } catch (error) {
    res.status(500).json({ message: 'Error registering user', error: error.message });
  }
});

module.exports = router;

Update server.js to use the auth routes:

javascript
const authRoutes = require('./routes/auth');
app.use('/api/auth', authRoutes);

User Authentication

Add a login route in auth.js:

javascript
router.post('/login', async (req, res) => {
  try {
    const { email, password } = req.body;

    const user = await User.findOne({ email });
    if (!user || !(await user.comparePassword(password))) {
      return res.status(401).json({ message: 'Invalid credentials' });
    }

    const token = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, { expiresIn: '1h' });

    res.json({ token });
  } catch (error) {
    res.status(500).json({ message: 'Error logging in', error: error.message });
  }
});

Create a middleware for authentication. Add a middleware folder and create auth.js:

javascript
const jwt = require('jsonwebtoken');

module.exports = (req, res, next) => {
  try {
    const token = req.headers.authorization.split(' ')[1];
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.userData = { userId: decoded.userId };
    next();
  } catch (error) {
    return res.status(401).json({ message: 'Authentication failed' });
  }
};

CRUD Operations

Create a users.js file in the routes folder:

javascript
const express = require('express');
const User = require('../models/User');
const authMiddleware = require('../middleware/auth');

const router = express.Router();

// Get user profile
router.get('/profile', authMiddleware, async (req, res) => {
  try {
    const user = await User.findById(req.userData.userId).select('-password');
    if (!user) {
      return res.status(404).json({ message: 'User not found' });
    }
    res.json(user);
  } catch (error) {
    res.status(500).json({ message: 'Error fetching user profile', error: error.message });
  }
});

// Update user profile
router.put('/profile', authMiddleware, async (req, res) => {
  try {
    const { username, email } = req.body;
    const user = await User.findByIdAndUpdate(
      req.userData.userId,
      { username, email },
      { new: true, runValidators: true }
    ).select('-password');
    res.json(user);
  } catch (error) {
    res.status(500).json({ message: 'Error updating user profile', error: error.message });
  }
});

// Delete user account
router.delete('/account', authMiddleware, async (req, res) => {
  try {
    await User.findByIdAndDelete(req.userData.userId);
    res.json({ message: 'User account deleted successfully' });
  } catch (error) {
    res.status(500).json({ message: 'Error deleting user account', error: error.message });
  }
});

module.exports = router;

Update server.js to use the user routes:

javascript
const userRoutes = require('./routes/users');
app.use('/api/users', userRoutes);

Security Best Practices

  1. Input Validation: Use a library like express-validator to validate and sanitize input.
  2. Rate Limiting: Implement rate limiting to prevent brute-force attacks.
  3. HTTPS: Always use HTTPS in production to encrypt data in transit.
  4. Password Hashing: We’re already using bcrypt for password hashing.
  5. JWT Best Practices: Use short expiration times for JWTs and implement a refresh token system for long-lived sessions.
  6. Error Handling: Implement proper error handling to avoid exposing sensitive information.
  7. Helmet: Use the helmet middleware to set various HTTP headers for security.

Install additional security packages:

bash
npm install express-validator express-rate-limit helmet

Update server.js with these security measures:

javascript
const { body, validationResult } = require('express-validator');
const rateLimit = require('express-rate-limit');
const helmet = require('helmet');

app.use(helmet());

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100 // limit each IP to 100 requests per windowMs
});
app.use(limiter);

// Example of input validation
router.post('/register', [
  body('username').trim().isLength({ min: 3 }).escape(),
  body('email').isEmail().normalizeEmail(),
  body('password').isLength({ min: 6 }),
], (req, res) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({ errors: errors.array() });
  }
  // ... rest of the registration logic
});

Testing the APIs

You can use tools like Postman or curl to test your APIs. Here’s an example using curl:

bash
# Register a new user
curl -X POST http://localhost:3000/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{"username":"testuser","email":"test@example.com","password":"password123"}'

# Login
curl -X POST http://localhost:3000/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"test@example.com","password":"password123"}'

# Get user profile (replace TOKEN with the actual token from login)
curl -X GET http://localhost:3000/api/users/profile \
  -H "Authorization: Bearer TOKEN"

Conclusion

In this guide, we’ve created a secure user registration API with CRUD operations using Node.js, Express.js, and MongoDB. We’ve implemented best practices for security, including password hashing, JWT authentication, and input validation.

Remember to always keep your dependencies updated and regularly audit your code for security vulnerabilities. As your application grows, consider implementing additional features like email verification, password reset functionality, and OAuth integration.

By following these practices, you’ll have a solid foundation for building secure and scalable user authentication systems in your Node.js applications.

Happy coding!

Hanzala — Software Developer🎓

Thank you for reading until the end. Before you go:


Recent Blogs

See All

Let's Build Together

Ready to turn your idea into reality?
Schedule a chat with me.

Hanzala - Self-Taught Developer