DEV Community

Abhay Singh Kathayat
Abhay Singh Kathayat

Posted on

2

Building an Advanced CRUD API with JWT Authentication, MongoDB, and Express.js

To build a more advanced CRUD API in Node.js, we can add several features such as:

  1. Database Integration: Instead of using an in-memory database, we’ll integrate a real database (e.g., MongoDB, PostgreSQL).
  2. Input Validation and Error Handling: We’ll use libraries like Joi or express-validator for input validation, and improve error handling.
  3. Authentication and Authorization: We’ll add JWT authentication to secure the API.
  4. API Documentation: We’ll integrate Swagger for API documentation.
  5. Environment Variables: We'll use dotenv for handling sensitive information like database credentials and API keys.

Here’s how you can implement this:

1. Setup Project and Install Dependencies

Create a new project and install the necessary dependencies:

mkdir advanced-crud-api
cd advanced-crud-api
npm init -y
Enter fullscreen mode Exit fullscreen mode

Install the following dependencies:

npm install express mongoose dotenv joi jsonwebtoken bcryptjs body-parser
npm install --save-dev nodemon
Enter fullscreen mode Exit fullscreen mode
  • express: The web framework.
  • mongoose: MongoDB ORM.
  • dotenv: For environment variable management.
  • joi: For input validation.
  • jsonwebtoken: For creating and verifying JWT tokens.
  • bcryptjs: For hashing passwords.
  • body-parser: Middleware for parsing request bodies.

2. Create Environment Configuration

Create a .env file to store sensitive data:

touch .env
Enter fullscreen mode Exit fullscreen mode

Add the following configuration to the .env file:

PORT=5000
DB_URI=mongodb://localhost:27017/advanced-crud
JWT_SECRET=your_jwt_secret_key
Enter fullscreen mode Exit fullscreen mode

3. Create Models and Database Configuration

Create a models folder and define a User model in models/User.js:

const mongoose = require('mongoose');

const userSchema = new mongoose.Schema({
  name: {
    type: String,
    required: [true, 'Name is required'],
  },
  email: {
    type: String,
    required: [true, 'Email is required'],
    unique: true,
    match: [/^[\w-]+(\.[\w-]+)*@([\w-]+\.)+[a-zA-Z]{2,7}$/, 'Please provide a valid email'],
  },
  password: {
    type: String,
    required: [true, 'Password is required'],
    minlength: [6, 'Password should be at least 6 characters long'],
  },
});

module.exports = mongoose.model('User', userSchema);
Enter fullscreen mode Exit fullscreen mode

In this model, we include validation rules for name, email, and password.

4. Database Connection Setup

Create a config/db.js file to set up the database connection:

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

dotenv.config();

const connectDB = async () => {
  try {
    await mongoose.connect(process.env.DB_URI, {
      useNewUrlParser: true,
      useUnifiedTopology: true,
    });
    console.log('MongoDB connected');
  } catch (error) {
    console.error('Error connecting to MongoDB:', error.message);
    process.exit(1); // Exit process with failure
  }
};

module.exports = connectDB;
Enter fullscreen mode Exit fullscreen mode

5. JWT Authentication Helper Functions

Create a file utils/auth.js for handling JWT generation and password hashing:

const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');

const generateToken = (id) => {
  return jwt.sign({ id }, process.env.JWT_SECRET, { expiresIn: '30d' });
};

const hashPassword = async (password) => {
  const salt = await bcrypt.genSalt(10);
  return await bcrypt.hash(password, salt);
};

const comparePasswords = async (password, hashedPassword) => {
  return await bcrypt.compare(password, hashedPassword);
};

module.exports = { generateToken, hashPassword, comparePasswords };
Enter fullscreen mode Exit fullscreen mode

6. Create Controllers for CRUD Operations

In the controllers folder, create userController.js to handle CRUD operations.

const User = require('../models/User');
const Joi = require('joi');
const { generateToken, hashPassword, comparePasswords } = require('../utils/auth');

// Validation schema
const userValidationSchema = Joi.object({
  name: Joi.string().min(3).required(),
  email: Joi.string().email().required(),
  password: Joi.string().min(6).required(),
});

// Register user
exports.registerUser = async (req, res) => {
  try {
    const { name, email, password } = req.body;

    // Validate input data
    const { error } = userValidationSchema.validate(req.body);
    if (error) return res.status(400).json({ error: error.details[0].message });

    // Check if the user already exists
    const userExists = await User.findOne({ email });
    if (userExists) {
      return res.status(400).json({ message: 'User already exists' });
    }

    const hashedPassword = await hashPassword(password);

    const newUser = new User({ name, email, password: hashedPassword });
    await newUser.save();

    const token = generateToken(newUser._id);

    res.status(201).json({ user: newUser, token });
  } catch (err) {
    res.status(500).json({ message: 'Server error' });
  }
};

// Login user
exports.loginUser = async (req, res) => {
  try {
    const { email, password } = req.body;

    // Validate input data
    const { error } = userValidationSchema.validate(req.body);
    if (error) return res.status(400).json({ error: error.details[0].message });

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

    const isMatch = await comparePasswords(password, user.password);
    if (!isMatch) return res.status(400).json({ message: 'Invalid email or password' });

    const token = generateToken(user._id);
    res.json({ user, token });
  } catch (err) {
    res.status(500).json({ message: 'Server error' });
  }
};

// Get user details
exports.getUserDetails = async (req, res) => {
  try {
    const user = await User.findById(req.user.id).select('-password');
    if (!user) return res.status(404).json({ message: 'User not found' });

    res.json(user);
  } catch (err) {
    res.status(500).json({ message: 'Server error' });
  }
};

// Update user details
exports.updateUser = async (req, res) => {
  try {
    const { name, email } = req.body;

    const user = await User.findByIdAndUpdate(req.user.id, { name, email }, { new: true });
    if (!user) return res.status(404).json({ message: 'User not found' });

    res.json(user);
  } catch (err) {
    res.status(500).json({ message: 'Server error' });
  }
};

// Delete user
exports.deleteUser = async (req, res) => {
  try {
    const user = await User.findByIdAndDelete(req.user.id);
    if (!user) return res.status(404).json({ message: 'User not found' });

    res.status(204).send();
  } catch (err) {
    res.status(500).json({ message: 'Server error' });
  }
};
Enter fullscreen mode Exit fullscreen mode

7. Protect Routes with Middleware

Create a middleware middleware/auth.js to protect routes that require authentication:

const jwt = require('jsonwebtoken');

const protect = (req, res, next) => {
  const token = req.header('Authorization')?.replace('Bearer ', '');

  if (!token) return res.status(401).json({ message: 'No token, authorization denied' });

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded; // Attach user info to request
    next();
  } catch (err) {
    res.status(401).json({ message: 'Token is not valid' });
  }
};

module.exports = protect;
Enter fullscreen mode Exit fullscreen mode

8. Setup Express Routes

Create the routes for user operations in routes/userRoutes.js:

const express = require('express');
const { registerUser, loginUser, getUserDetails, updateUser, deleteUser } = require('../controllers/userController');
const protect = require('../middleware/auth');

const router = express.Router();

router.post('/register', registerUser);
router.post('/login', loginUser);
router.get('/me', protect, getUserDetails);
router.put('/me', protect, updateUser);
router.delete('/me', protect, deleteUser);

module.exports = router;
Enter fullscreen mode Exit fullscreen mode

9. Set Up the Main App

Now, set up the main server file in index.js:

const express = require('express');
const dotenv = require('dotenv');
const connectDB = require('./config/db');
const userRoutes = require('./routes/userRoutes');

dotenv.config();

const app = express();

// Connect to database
connectDB();

app.use(express.json()); // Parse incoming JSON requests
app.use('/api/users', userRoutes); // Use the user routes

const PORT = process.env.PORT || 5000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
Enter fullscreen mode Exit fullscreen mode

10. Run the Server

Finally, start the

server:

npm run dev
Enter fullscreen mode Exit fullscreen mode

You now have a fully functional advanced CRUD API with JWT authentication, user registration, and error handling.

Conclusion

This advanced setup covers:

  • MongoDB integration.
  • Input validation with Joi.
  • JWT-based authentication and authorization.
  • User registration, login, update, and deletion operations.
  • Secure password handling with bcrypt.

Image of Datadog

Create and maintain end-to-end frontend tests

Learn best practices on creating frontend tests, testing on-premise apps, integrating tests into your CI/CD pipeline, and using Datadog’s testing tunnel.

Download The Guide

Top comments (0)

The best way to debug slow web pages cover image

The best way to debug slow web pages

Tools like Page Speed Insights and Google Lighthouse are great for providing advice for front end performance issues. But what these tools can’t do, is evaluate performance across your entire stack of distributed services and applications.

Watch video

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay