Day 4: Workshop Guide for Setting Up a Backend with Express and MongoDB

Day 4: Workshop Guide for Setting Up a Backend with Express and MongoDB

Begin by creating a new project directory and initializing it with npm init:

mkdir blog-backend
cd blog-backend
npm init -y

This will create a package.json file for your project. Afterward, install the necessary dependencies with:

npm install express body-parser dotenv mongoose cookie-parser morgan nodemon bcrypt jsonwebtoken express-validator express-async-handler slugify

These packages include everything we need for creating and securing routes, handling requests, interacting with MongoDB, and managing cookies.

The basic structure of your project should look like this:

blog-backend/
├── config/
│   └── dbConnect.js
├── routes/
│   ├── authRoute.js
│   ├── productRoute.js
│   └── blogRoute.js
├── index.js
├── .env
└── package.json
  • config/dbConnect.js: For setting up the MongoDB connection.

  • routes/: Directory for handling different routes (auth, product, blog).

  • index.js: The main entry point for your application.

In the config/dbConnect.js file, establish a connection to MongoDB using Mongoose:

// dbconnect.js
import dotenv from "dotenv";
import mongoose from "mongoose";

dotenv.config();

export const dbConnect = async () => {
  try {
    console.log(
      "🚀 ~ dbConnect ~ process.env.MONGO_URI:",
      process.env.MONGO_URI
    );
    await mongoose.connect(process.env.MONGO_URI, {
      useNewUrlParser: true,
      useUnifiedTopology: true,
    });
    console.log("MongoDB connected successfully");
  } catch (error) {
    console.error("MongoDB connection failed");
  }
};
  • Make sure to create a .env file and add your MongoDB connection string:

      PORT=8000
      MONGO_URI=mongodb://localhost:27017/myDatabase
    

In the index.js file, we will set up the server using Express.js:

// index.js
import express from "express";
import dotenv from "dotenv";
import bodyParser from "body-parser";
import cookieParser from "cookie-parser";
import { dbConnect } from "./config/dbconnect.js";

dotenv.config();
const app = express();
const port = process.env.PORT || 3000;

// Body parser is used to parse the incoming request bodies in a middleware before you handle it.
app.use(cookieParser());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

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

dbConnect();
  • Morgan logs HTTP requests.

  • Body-parser allows us to parse incoming requests in JSON and URL-encoded formats.

Cookie-parser enables us to work with cookies.

Now if we run the server it must need to be notified with “MongoDB connected successfully”. If the database is not connected then you must need to ensure that it should be connected before proceeding to next step.

Create a model with name userModel.js which willl create a table in mongoDB where we will perform the crud operation from controller

// models/userModel.js
import mongoose from "mongoose";

var userSchema = new mongoose.Schema(
  {
    firstname: {
      type: String,
      required: true,
      trim: true,
      min: 3,
      max: 20,
    },
    lastname: {
      type: String,
      required: true,
      trim: true,
      min: 3,
      max: 20,
    },
    username: {
      type: String,
      required: true,
      trim: true,
      unique: true,
      index: true,
      lowercase: true,
    },
    email: {
      type: String,
      required: true,
      trim: true,
      unique: true,
      lowercase: true,
    },
    password: {
      type: String,
      required: true,
    },
  },
  {
    timestamps: true,
  }
);

export default mongoose.model("User", userSchema);

Create the route files for authentication, products, and blogs. Each route file will contain the Express routing logic for handling requests.

Example for controller/UserController.js:

For testing: You canPlay with below code

import { validationResult } from "express-validator";
import expressAsyncHandler from "express-async-handler";

export const createUser = expressAsyncHandler(async (req, res) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({ errors: errors.array() });
  }
  res.send("User created successfully");
});

Before initiating any CRUD operations, please ensure that your route is functioning correctly. Once confirmed, you can proceed with performing the CRUD operations.

// UserController.js
import { validationResult } from "express-validator";
import User from "../models/userModel.js";
import expressAsyncHandler from "express-async-handler";
import { generateToken } from "../config/jwtToken.js";

export const createUser = async (req, res) => {
  try {
    const errors = validationResult(req);
    console.log("🚀 ~ createUser ~ req:", req.body);
    if (!errors.isEmpty()) {
      return res.status(400).json({
        errors: errors.array(),
        message: "validation error",
        success: false,
      });
    }
    //check if user already exists

    const isExist = await User.findOne({
      email: req.body.email,
    });
    console.log("🚀 ~ createUser ~ isExist:", isExist);
    if (isExist) {
      return res.status(400).json({
        message: "User already exists",
        success: false,
      });
    }

    const user = await User.create(req.body);

    return res.status(201).json({
      message: "User created successfully",
      success: true,
      data: user,
    });
  } catch (error) {
    console.error(
      "🚀 ~ file: UserController.js ~ line 13 ~ createUser ~ error",
      error
    );
  }
};

export const updateUser = expressAsyncHandler(async (req, res) => {
  try {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({
        errors: errors.array(),
        message: "validation error",
        success: false,
      });
    }

    const user = await User.findByIdAndUpdate(req.params.id, req.body, {
      new: true,
    });

    return res.status(200).json({
      message: "User updated successfully",
      success: true,
      data: user,
    });
  } catch (errro) {
    return res.status(400).json({
      message: "User not found",
      success: false,
    });
  }
});

export const getAllUsers = expressAsyncHandler(async (req, res) => {
  try {
    const users = await User.find();
    return res.status(200).json({
      message: "Users fetched successfully",
      success: true,
      data: users,
    });
  } catch (error) {
    console.error("🚀 ~ getAllUsers ~ error", error);
    return res.status(500).json({
      message: "Server error",
      success: false,
    });
  }
});

export const deleteUser = expressAsyncHandler(async (req, res) => {
  try {
    const user = await User.findById(req.params.id);

    if (!user) {
      return res.status(404).json({
        message: "User not found",
        success: false,
      });
    }

    await User.findByIdAndDelete(req.params.id);

    return res.status(200).json({
      message: "User deleted successfully",
      success: true,
    });
  } catch (error) {
    console.error("🚀 ~ deleteUser ~ error", error);
    return res.status(500).json({
      message: "Server error",
      success: false,
    });
  }
});

Create the route files for authentication, products, and blogs. Each route file will contain the Express routing logic for handling requests. Likewise, the controller is imported in to the rooutes so whenever the route is hitted with certain http method it we response the desired output

Example for routes/authRoute.js:

//authRoute.js
import express from "express";
import { createUser } from "../controller/UserController.js";

const router = express.Router();

router.post("/createUser", createUser);

export { router as authRouter };

You can follow a similar structure for the productRoute.js and blogRoute.js.

// index.js
import express from "express";
import dotenv from "dotenv";
import bodyParser from "body-parser";
import cookieParser from "cookie-parser";
import { dbConnect } from "./config/dbconnect.js";
import { authRouter } from "./routes/authRoute.js";

dotenv.config();
const app = express();
const port = process.env.PORT || 3000;

// Body parser is used to parse the incoming request bodies in a middleware before you handle it.
app.use(cookieParser());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

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

app.use("/api/auth", authRouter);
dbConnect();

If you want some validation

import { body } from "express-validator";

export const validateUser = [
  body("firstname")
    .trim()
    .notEmpty()
    .withMessage("Firstname is required")
    .isAlpha()
    .withMessage("Firstname must only contain letters")
    .isLength({ min: 2, max: 50 })
    .withMessage("Firstname must be between 2 and 50 characters"),

  body("lastname")
    .trim()
    .notEmpty()
    .withMessage("Lastname is required")
    .isAlpha()
    .withMessage("Lastname must only contain letters")
    .isLength({ min: 2, max: 50 })
    .withMessage("Lastname must be between 2 and 50 characters"),

  body("username")
    .trim()
    .notEmpty()
    .withMessage("Username is required")
    .isAlphanumeric()
    .withMessage("Username must only contain letters and numbers")
    .isLength({ min: 3, max: 30 })
    .withMessage("Username must be between 3 and 30 characters"),

  body("email")
    .trim()
    .notEmpty()
    .withMessage("Email is required")
    .isEmail()
    .withMessage("Invalid email format"),

  body("password")
    .notEmpty()
    .withMessage("Password is required")
    .isLength({ min: 8 })
    .withMessage("Password must be at least 8 characters long")
    .matches(/[A-Z]/)
    .withMessage("Password must contain at least one uppercase letter")
    .matches(/[a-z]/)
    .withMessage("Password must contain at least one lowercase letter")
    .matches(/[0-9]/)
    .withMessage("Password must contain at least one number")
    .matches(/[!@#$%^&*]/)
    .withMessage("Password must contain at least one special character"),
];
import express from "express";
import { createUser } from "../controller/UserController.js";
import { validateUser } from "../validators/userValidator.js";

const router = express.Router();

router.get("/login", createUser);
router.post("/createUser", validateUser, createUser);

export { router as authRouter };

Create a middleware to authenticate the user. It check if the current user trying to perform the operation is autenticated user not.

// middleware/authMiddleware.js
import User from "../models/userModel.js";
import jwt from "jsonwebtoken";
import expressAsyncHandler from "express-async-handler";

export const authMiddleware = expressAsyncHandler(async (req, res, next) => {
  let token;
  if (req?.headers?.authorization?.startsWith("Bearer")) {
    token = req.headers.authorization.split(" ")[1];
    if (token) {
      const decoded = jwt.verify(token, process.env.JWT_SECRET);
      const user = await User.findById(decoded?.id);
      req.user = user;
      next();
    }
  } else {
    return res.status(401).json({
      message: "Not authorized token, Please login again",
      success: false,
    });
  }
});

authMiddleware is kept at the createUser Route or you can add it on any route as per your requirement which is middleware to secure action that we need perform.

import express from "express";
import {
  createUser,
  deleteUser,
  getAllUsers,
  updateUser,
  userLogin,
} from "../controller/UserController.js";
import { authMiddleware } from "../middleware/authMiddleware.js";

const router = express.Router();

router.post("/createUser", authMiddleware, createUser);
router.put("/updateUser/:id", updateUser);
router.get("/get-all", getAllUsers);
router.delete("/delete/:id", deleteUser);
router.post("/login", userLogin);

export { router as authRouter };

To test if your server is running, start it by running:

npm run server

If everything is set up correctly, you should see:

Server is listening on port http://localhost:5000
// login user in UserController.js
import { generateToken } from "../config/jwtToken.js";

export const userLogin = expressAsyncHandler(async (req, res) => {
  const { email, password } = req.body;
  // findUser variable include all the information of that user with that email.
  const findUser = await User.findOne({ email });
  const accessToken = generateToken(findUser._id);
  // ava store garam  token lai cookie ma
  res.cookie("accessToken", accessToken, {
    httpOnly: true,
    maxAge: 72 * 60 * 60 * 1000,
  });
  if (findUser && (await findUser.isPasswordMatched(password))) {
    // res.json(findUser);
    res.json({
      // ?. syntax is called optional chaining introduced on ecma script in 2020
      _id: findUser?._id,
      firstname: findUser?.firstname,
      lastname: findUser?.lastname,
      email: findUser?.email,
      mobile: findUser?.mobile,
      token: generateToken(findUser?._id),
    });
  } else {
    throw new Error("Invalid Credentials");
  }
});

For that we need to generate AccessToken using JWT for Authentication

//config/jwtToken.js
import jwt from "jsonwebtoken";

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

Also in models/UserModel.js add the following to encrypt and check password

// Yo code chai password lai encrypt garna lai
userSchema.pre("save", async function (next) {
  if (!this.isModified("password")) {
    next();
  }
  const salt = await bcrypt.genSaltSync(10);
  this.password = await bcrypt.hash(this.password, salt);
});

// password match vaxa ki nai vanera check garna
userSchema.methods.isPasswordMatched = async function (enteredPassword) {
  return await bcrypt.compare(enteredPassword, this.password);
};

Overall, which will look like this

// models/userModel.js
import mongoose from "mongoose";
import bcrypt from "bcryptjs";

var userSchema = new mongoose.Schema(
  {
    firstname: {
      type: String,
      required: true,
      trim: true,
      min: 3,
      max: 20,
    },
    lastname: {
      type: String,
      required: true,
      trim: true,
      min: 3,
      max: 20,
    },
    username: {
      type: String,
      required: true,
      trim: true,
      unique: true,
      index: true,
      lowercase: true,
    },
    email: {
      type: String,
      required: true,
      trim: true,
      unique: true,
      lowercase: true,
    },
    password: {
      type: String,
      required: true,
    },
  },
  {
    timestamps: true,
  }
);

// Yo code chai password lai encrypt garna lai
userSchema.pre("save", async function (next) {
  if (!this.isModified("password")) {
    next();
  }
  const salt = await bcrypt.genSaltSync(10);
  this.password = await bcrypt.hash(this.password, salt);
});

// password match vaxa ki nai vanera check garna
userSchema.methods.isPasswordMatched = async function (enteredPassword) {
  return await bcrypt.compare(enteredPassword, this.password);
};

export default mongoose.model("User", userSchema);