DEV Community

Abhay Singh Kathayat
Abhay Singh Kathayat

Posted on

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.

Top comments (0)