Ecommerce Backend with Javascript

Ecommerce Backend with Javascript

Recently, I was fortunate enough to be accepted as an intern at iNeuron, a tech-based company in Bengaluru. As part of my software engineering development, I was assigned a passion project to work on independently, in addition to collaborating on a team project.

For my passion project, I decided to build an E-commerce API. In this tutorial, I will guide you through the steps I took to create a simple yet functional API. While it may not include all the advanced features found in modern E-commerce web applications, it provides a solid foundation to get you started. You can further enhance and customize this API by adding additional features to meet your specific requirements.

Prerequisites.

  • Intermediate understanding of Javascript.

  • Basic knowledge of Nodejs, MongoDB, Mongoose and Express.

Basic Features.

  • registering and login functionality. For All users

  • getting, updating, and deleting any user. For All Admin

  • checking products For All users

  • adding, deleting, updating, and getting products. For All Admin

  • ordering items. For All users

  • editing, updating and getting all ptoduct. For All Admin

Packages used.

  "dependencies": {
    "bcryptjs": "^2.4.3",
    "cloudinary": "^1.36.1",
    "cookie-parser": "^1.4.6",
    "cors": "^2.8.5",
    "dotenv": "^16.0.3",
    "express": "^4.18.2",
    "express-fileupload": "^1.4.0",
    "jsonwebtoken": "^9.0.0",
    "mongoose": "^7.0.3",
    "morgan": "^1.10.0",
    "nodemailer": "^6.9.1",
    "nodemon": "^2.0.22"
  }

The above dependencies/ packages are needed for making an Ecommerce backend project. I know this is overwhelming so I will give a brief explanation on what are their purpose.

bcryptjs:
bcrypt.js is a widely used library for hashing and salting passwords in JavaScript. It provides a simple API for encrypting and comparing passwords securely.

installation

npm install bcryptjs

cloudinary:
Cloudinary is a cloud-based media management platform that provides a wide range of features for uploading, storing, manipulating, and delivering media assets such as images and videos. It offers a comprehensive set of APIs and tools for managing media files in various formats

installation

npm install cloudinary

cookie-parser:

cookie-parser is a middleware for Express.js that parses HTTP cookies from the request object and makes them available in the req.cookies property. It simplifies the process of working with cookies in your Express.js applications.

installation

npm install cookie-parser

cors:

Enabling CORS with the cors middleware in your Express.js application allows your API to be accessed by clients running on different domains. It helps in preventing cross-origin security issues while still allowing controlled access to your resources.

installation

npm install cors

Dotenv:

dotenv is a popular npm package used in Node.js applications to load environment variables from a .env file into the process.env object. It provides a convenient way to manage and access configuration values in your application without hardcoding them in your codebase.

installation

npm install dotenv

express:

Express is a popular web application framework for Node.js. It provides a minimalistic and flexible set of features to build web applications and APIs. Express.js simplifies the process of handling HTTP requests, routing, middleware integration, and more.

npm install express

express-fileupload:

express-fileupload is a middleware package for the Express.js web framework that simplifies handling file uploads. It provides an easy-to-use interface for managing file uploads in Express applications without the need for additional libraries like multer.

installation

npm install express-fileupload

jsonwebtoken:

The jsonwebtoken library is used to generate and verify JSON Web Tokens (JWTs) in web applications. JSON Web Tokens are a compact and self-contained way to securely transmit information between parties as a JSON object. They are commonly used for authentication and authorization purposes.

installation:

npm i jsonwebtoken

Mongoose:

Mongoose is an Object Data Modeling (ODM) library for Node.js and MongoDB. It provides a higher-level, schema-based approach for interacting with MongoDB databases, making it easier to define and work with data models in your Node.js applications.

installation

npm i mongoose

morgan:

Morgan helps you gain insights into your application's request-response flow, diagnose issues, and monitor traffic. It is a valuable tool for debugging and analyzing the behavior of your web application.

installation

npm i morgan

Nodemailer:

Nodemailer simplifies the process of sending emails from Node.js applications and provides flexibility and customization options for handling various email requirements. It is widely used in applications that involve email notifications, user verification, password reset, and other email-based functionalities.

installation

npm i nodemailer

nodemon:

Nodemon is a tool that helps in the development of Node.js applications by automatically restarting the server whenever changes are made to the source code. It is particularly useful during the development process, as it eliminates the need to manually stop and restart the server after each code modification.

installation

npm i nodemon

Folder and file structure.

The picture below shows the folder and file structure. We should organize files and folders for better understanding and for future reference.

Note: It is not necessary to organize the file like above different project involve different folder and file structure. separating folder prevents from code looking ugly and help for better understanding.

Mindmaping.

Mindmaping.Mind mapping is a visual technique that helps to organize and represent information in a hierarchical and interconnected manner. It allows you to capture and structure ideas, concepts, and relationships, making it easier to understand, analyze, and remember information.

Mindmaping models.

The image is the mindmaping/ brainstroming about the requirement in the models.

Code.

Create a folder name server. Go to the terminal under the directory and do the installation as mentioned below.

Installation

  1. npm init -y

  2. install all the package that is mentioned above.

Models

Create folder name Models. Inside it create the file as below.

  1. Create a file name user.schema.js.
import mongoose from 'mongoose';
import AuthRoles from '../utils/authRoles.js';
import bcrypt from 'bcryptjs'
import JWT from 'jsonwebtoken'
import crypto from 'crypto'
import config from '../config/index.js'

const userSchema = mongoose.Schema(
    {
        name: {
            type: String,
            required: [true, "Name is required"],
            maxLength: [50, "Name must be less than 50"]
        },
        email: {
            type: String,
            required: [true, "Email is required"],
            unique: true
        },
        password: {
            type: String,
            required: [true, "Password is required"],
            minLength: [7, "Password must be atleast 7 characters"],
            select: false
        },
        roles: {
            type: String,
            enum: Object.values(AuthRoles),
            default: AuthRoles.USER,
        },
        forgotPasswordToken: String,
        forgotPasswordExpiry: Date,
    },
    {
        timestamps: true
    }
);

// challange 1- encrypt the password.   -hooks
userSchema.pre('save', async function (next) {
    if (!this.isModified("password")) return next()
    this.password = await bcrypt.hash(this.password, 10)
    next()
})

// add more features directly to your schemas.

userSchema.methods = {
    // compare password
    comparePassword: async function (enteredPassword) {
        return await bcrypt.compare(enteredPassword, this.password)
    },

    // generate JWT token
    getJwtToken: function () {
        return JWT.sign(
            {
                _id: this._id,
                role: this.role
            },
            config.JWT_SECRET,
            {
                expiresIn: config.JWT_EXPIRY
            }
        )
    },

    generateForgotPasswordToken: function (){
        const forgotToken  = crypto.randomBytes(20).toString('hex');

        // step 1 : save to db
        this.forgotPasswordToken = crypto
        .createHash("sha256")
        .update(forgotToken)
        .digest('hex')

        this.forgotPasswordExpiry = Date.now() + 20 * 60 * 1000
        // step 2 : return values to user

        return forgotToken

    }

}

export default mongoose.model("User", userSchema)
  1. Create file name product.schema.js
import mongoose from "mongoose";

const productSchema = new mongoose.Schema(
    {
       name: {
            type: String,
            required: [true , "Please provide a product name"],
            trim: true,
            maxLength: [120 , " Product name must not be greater than 120 characters"]
        },
        price: {
            type: Number,
            required: [true , "Please provide a product price"],
            maxLength: [5, " Product price must not be greater than 5 digit"]
        },
        description: {
            type: String,
            required: [true, "Please provide product description"]
            // use some form of editor  - personal challange

        },
        photos:[
            {
                id : {
                    type: String,
                    required: true
                },
                secure_url: {
                    type: String,
                    required: true
                }
            }
        ],

        // updated at the time of order controller.
        stock: {
            type: Number,
            // default: 0
            required: [true, "Please a number in stock"]
        },
        sold: {
            type: Number,
            default: 0
        },
        collectionId: {
            type: mongoose.Schema.Types.ObjectId,
            ref: "Collection"
        }
    },
    {
        timestamps: true
    }
)

export default mongoose.model("Product", productSchema)
  1. Create a file name collection.schema.js
import mongoose from "mongoose";

const collectionSchema = new mongoose.Schema(
    {
        name:{
            type: String,
            required: [true, "Please provide a category name"],
            trim:true,
            maxLength:[120, "Collection name should not be more than 120 characters"],
        },
    },
    {
        timestamps: true
    }
);

export default mongoose.model ("Collection", collectionSchema);
  1. Create a file name order.schema.js
import mongoose from 'mongoose';

const orderSchema = new mongoose.Schema({
    shippingInfo: {
      address: {
        type: String,
        required: true,
      },
      city: {
        type: String,
        required: true,
      },
      phoneNo: {
        type: String,
        required: true,
      },
      postalCode: {
        type: String,
        required: true,
      },
      state: {
        type: String,
        required: true,
      },
      country: {
        type: String,
        required: true,
      },
    },
    user: {
      type: mongoose.Schema.ObjectId, //mongoose.Schema.Types.ObjectId
      ref: "User",
      required: true,
    },
    orderItems: [
      {
        name: {
          type: String,
          required: true,
        },
        quantity: {
          type: Number,
          required: true,
        },
        image: {
          type: String,
          required: true,
        },
        price: {
          type: Number,
          required: true,
        },
        product: {
          type: mongoose.Schema.ObjectId, //mongoose.Schema.Types.ObjectId
          ref: "Product",
          required: true,
        },
      },
    ],
    paymentInfo: {
      id: {
        type: String,
      },
    },
    taxAmount: {
      type: Number,
      required: true,
    },
    shippingAmount: {
      type: Number,
      required: true,
    },
    totalAmount: {
      type: Number,
      required: true,
    },
    orderStatus: {
      type: String,
      required: true,
      default: "processing",
    },
    deliveredAt: {
      type: Date,
    },
    createdAt: {
      type: Date,
      default: Date.now,
    },
  });

  export default mongoose.model("Order", orderSchema)

Utils

Create folder name utils. Inside it create the file as below.

  1. Create a file name authRoles.js

    ```javascript const AuthRoles ={ ADMIN:"ADMIN", MODERATOR:'MODERATOR', USER: "USER" }

export default AuthRoles;


    1. Create a file customError.js


    ```javascript
    class CustomError extends Error {
        constructor(message, code){
            super(message)
            this.code = code
        }
    }

    export default CustomError
  1. Create a file mailHelper.js
    import transporter from "../config/transporter.config.js";
    import config from "../config/index.js";


    const mailHelper = async(options) =>{
        const message ={
            from: config.SMTP_MAIL_EMAIL , // sender address
            to: options.email, // list of receivers
            subject: options.subject, // Subject line
            text: options.text, // plain text body
            // html: "<b>Hello world?</b>", // html body
        }

        await transporter.sendMail(message)
    }

    export default mailHelper
  1. Create a file name whereClause.js
    // base - Product.find()
    // base - Product.find(email: {"hitesh@lco.dev"})

    //bigQ - //search=coder&page=2&category=shortsleeves&rating[gte]=4
    // &price[lte]=999&price[gte]=199&limit=5

    class WhereClause {
        constructor(base, bigQ) {
          this.base = base;
          this.bigQ = bigQ;
        }

        search() {
          const searchword = this.bigQ.search
            ? {
                name: {
                  $regex: this.bigQ.search,
                  $options: "i",
                },
              }
            : {};

          this.base = this.base.find({ ...searchword });
          return this;
        }

        filter() {
          const copyQ = { ...this.bigQ };

          delete copyQ["search"];
          delete copyQ["limit"];
          delete copyQ["page"];

          //convert bigQ into a string => copyQ
          let stringOfCopyQ = JSON.stringify(copyQ);

          stringOfCopyQ = stringOfCopyQ.replace(
            /\b(gte|lte|gt|lt)\b/g,
            (m) => `$${m}`
          );

          const jsonOfCopyQ = JSON.parse(stringOfCopyQ);

          this.base = this.base.find(jsonOfCopyQ);
          return this;
        }

        pager(resultperPage) {
          let currentPage = 1;
          if (this.bigQ.page) {
            currentPage = this.bigQ.page;
          }

          const skipVal = resultperPage * (currentPage - 1);

          this.base = this.base.limit(resultperPage).skip(skipVal);
          return this;
        }
      }

      export default WhereClause;

Services

Create a folder name services. Inside it create the file as below.

  1. Create a file name tryCatchHandler.js
const tryCatchHandler = (fn) => async(req, res, next) => {

    try {
       await fn(req,res,next)
    }
    catch(error) {
        res.status(error.code || 500).json({
            success:false,
            message: error.message
        })
    }
}

export default tryCatchHandler

Config

Create a folder name config. Inside it create the file as below.

  1. Create a file name transporter.config.js
import nodemailer from 'nodemailer'
import config from './index.js'

let transporter = nodemailer.createTransport({
    host: config.SMTP_MAIL_HOST,
    port: config.SMTP_MAIL_PORT,
    secure: false, // true for 465, false for other ports
    auth: {
      user: config.SMTP_MAIL_USERNAME, // generated ethereal user
      pass: config.SMTP_MAIL_PASSWORD, // generated ethereal password
    },
});

export default transporter
  1. Create a file name index.js
import dotenv from 'dotenv'

dotenv.config()

const config = {
    JWT_SECRET: process.env.JWT_SECRET,
    JWT_EXPIRY: process.env.JWT_EXPIRY || '30d',
    MONGODB_URL: process.env.MONGODB_URL || 'ok',
    PORT: process.env.PORT,
    SMTP_MAIL_HOST: process.env.SMTP_MAIL_HOST,
    SMTP_MAIL_PORT: process.env.SMTP_MAIL_PORT,
    SMTP_MAIL_USERNAME: process.env.SMTP_MAIL_USERNAME,
    SMTP_MAIL_PASSWORD: process.env.SMTP_MAIL_PASSWORD,
    SMTP_MAIL_EMAIL: process.env.SMTP_MAIL_EMAIL,

    CLOUDINARY_NAME: process.env.CLOUDINARY_NAME,
    CLOUDINARY_API_KEY: process.env.CLOUDINARY_API_KEY,
    CLOUDINARY_API_SECRET: process.env.CLOUDINARY_API_SECRET
}


export default config

Middleware

Create a folder name middlewares. Inside it create the file as below.

  1. Create a file auth.middlewae.js

    ```javascript import User from '../models/user.schema.js' import JWT from 'jsonwebtoken' import tryCatchHandler from '../services/tryCatchHandler.js' import CustomError from '../utils/customError.js'

    import AuthRoles from '../utils/authRoles.js'

    import config from "../config/index.js"

    export const isLoggedIn = tryCatchHandler(async(req,res,next)=>{ let token;

    if( req.cookies.token || (req.headers.authorization && req.headers.authorization.startsWith('Bearer'))) { token = req.cookies.token || req.headers.authorization.split(" ") [1] }

    if (!token){ throw new CustomError('Not authorized to excess this route', 401) }

    try{ const decodedJwtPayload = JWT.verify(token, config.JWT_SECRET)

    // _id, find user based on id , set this in req.user req.user = await User.findById(decodedJwtPayload._id, "name email roles") next() } catch (error){ throw new CustomError('Not authorized to access this route', 401) } })

export const customRole = (authrole) => { console.log(authrole) return (req, res, next) => { console.log(req.user.roles) if (authrole !== req.user.roles) { return next(new CustomError(${req.user.roles}You are not allowed for this resouce, 403)); } next(); }; };



### <mark>Controller</mark>

*Create a folder name controllers. Inside it create the file as below.*

1. Create a file name auth.controller.js


```javascript
import User from '../models/user.schema.js'
import tryCatchHandler from '../services/tryCatchHandler.js'
import CustomError from '../utils/customError.js'

import mailHelper from '../utils/mailHelper.js'

import crypto from 'crypto'





export const cookieOptions={
    expires: new Date (Date.now() + 3 * 24 * 60 *60 * 1000),
    httpOnly: true,

    // could be in separate filee in utils 
}


/****************************************************************************************
 * @SignUp
 * @route http://localhost:4000/api/auth/signup
 * @description User signup Controller for creating new user
 * @parameter name , email ,password
 * @returns User Object

 * *********************************************************************************** */


export const signUp= tryCatchHandler (async (req, res)=>{
    const {name, email, password} = req.body

    if(!name || !email || !password){
        throw new CustomError('Please fill all fields', 400)
    }

    // check if user exists

    const existingUser =await User.findOne({email})

    if (existingUser){
        throw new CustomError('User alreaady exists',400)
    }

    const user = await User.create({
        name,
        email,
        password
    })

    const token = user.getJwtToken()
    console.log(user);
    user.password = undefined

    res.cookie("token", token, cookieOptions);
    res.status(200).json({
        success:true,
        token,
        user
    })
})

/****************************************************************************************
 * @LOGIN
 * @route http://localhost:4000/api/auth/login
 * @description User signIn Controller for loging new user
 * @parameter  email ,password
 * @returns User Object

 * *********************************************************************************** */

export const login = tryCatchHandler (async(req,res) =>{
    const {email, password} = req.body

    if( !email || !password){
        throw new CustomError('Please fill all fields', 400)
    }

    const user= await User.findOne({email}).select("+password")

    if(!user){
        throw new CustomError('Invalid credentials', 400)
    }

    const isPasswordMatched =await user.comparePassword(password)

    if(isPasswordMatched){
        const token = user.getJwtToken()
        user.password = undefined;
        res.cookie("token", token, cookieOptions)
        return res.status(200).json({
            success:true,
            token,
            user
        })
    }

    throw new CustomError('Invalid credentials - pass', 400)
})

/***************************************************************************************
 * @LOGOUT
 * @route http://localhost:4000/api/auth/logout
 * @description: User signout by clearing cookie
 * @parameters 
 * @return success message
 ***************************************************************************************/

export const logout = tryCatchHandler(async (_req, res) =>{
    // res.clearCookie()
    res.cookie("token", null, {
        expires: new Date(Date.now()),
        httpOnly: true
    })
    res.status(200).json({
        success: true,
        message: "Logged Out"
    })
})

/***************************************************************************************
 * @FORGOT_PASSWORD
 * @route http://localhost:4000/api/auth/password/forgot
 * @description: User will submit email and we will generate a token
 * @parameters email
 * @return success message - email send
 ***************************************************************************************/


export const forgotPassword = tryCatchHandler(async (req,res) =>{
    const {email} = req.body
    // check email for null or '' 
    const user = await User.findOne({email})

    if (!user){
        throw new CustomError("User not found", 404)
    }
    const resetToken = user.generateForgotPasswordToken()

    await user.save({validateBeforeSave : false})

    const resetUrl =
    `${req.protocol}://${req.get("host")}/api/auth/password/reset/${resetToken}`

    const text = `Your password reset url is  \n\n ${resetUrl}\n\n`

    try {
        await mailHelper({
            email:user.email,
            subject: "Password reset email for website",
            text:text,
        })
        res.status(200).json({
            success:true,
            message: `Email send to ${user.email}`
        })
    } catch (error) {
        // rollback - clear fields and save.
        user.forgotPasswordToken = undefined
        user.forgotPasswordExpiry= undefined

        await user.save({validateBeforeSave: false})

        throw new CustomError(error.message || 'Email sent failure')
    }
})

/***************************************************************************************
 * @RESET_PASSWORD
 * @route http://localhost:4000/api/auth/password/reser/:resetPasswordToken
 * @description: User will be able to reset password based on url token
 * @parameters token from url, password and confirmpass
 * @return User object
 ***************************************************************************************/


export const resetPassword = tryCatchHandler(async(req, res) =>{
    const {token: resetToken} = req.params
    const {password , confirmPassword} = req.body


    const resetPasswordToken =crypto
    .createHash('')
    .update(resetToken)
    .digest('hex')

    const user= await User.findOne({
        forgotPasswordToken: resetPasswordToken,
        forgotPasswordExpiry: {$gt:Date.now()}
    })

    if(!user){
        throw new CustomError('password token is invalid or expired', 400)
    }

    if (password !== confirmPassword){
        throw new CustomError(' Password and confirm Password does not matched', 400)
    }

    user.password = password
    user.forgotPasswordToken = undefined
    user.forgotPasswordExpiry= undefined

    await user.save()

    // create token and send as response 
    const token = user.getJwtToken()
    user.password = undefined

    // helper method for cookie can be added
    res.cookie('token',token , cookieOptions)

    res.status(200).json({
        success:true,
        user
    })
})

// TODO: create controller for change password

/***************************************************************************************
 * @GET_PROFILE
 * @REQUEST_TYPE GET
 * @route http://localhost:4000/api/auth/profile
 * @description: check for token and populate req.user
 * @parameters 
 * @return User object
 ***************************************************************************************/

export const getProfile = tryCatchHandler(async(req, res)=>{
    // req.user
    const {user} =  req
    if (!user){
        throw new CustomError('user not  found ', 404)
    }

    res.status(200).json({
        success:true,
        user
    })
})


// admin routes

export const adminAllUser = tryCatchHandler(async (req, res, next) => {
    // select all users
    const users = await User.find();

    // send all users
    res.status(200).json({
      success: true,
      users,
    });
  });

export const admingetOneUser = tryCatchHandler(async (req, res, next) => {
    // get id from url and get user from database
    const user = await User.findById(req.params.id);

    if (!user) {
      next(new CustomError("No user found", 400));
    }

    // send user
    res.status(200).json({
      success: true,
      user,
    });
  });

  export const adminUpdateOneUserDetails = tryCatchHandler(async (req, res, next) => {
    // add a check for email and name in body

    // get data from request body
    const newData = {
      name: req.body.name,
      email: req.body.email,
      roles: req.body.roles,
    };

    // update the user in database
    const user = await User.findByIdAndUpdate(req.params.id, newData, {
      new: true,
      runValidators: true,
      useFindAndModify: false,
    });

    res.status(200).json({
      success: true,
    });
  });

  export const adminDeleteOneUser = tryCatchHandler(async (req, res, next) => {
    // get user from url
    const user = await User.findById(req.params.id);

    if (!user) {
      return next(new CustomError("No Such user found", 401));
    }



    // remove user from databse
    await user.deleteOne();

    res.status(200).json({
      success: true,
    });
  });
  1. Create a file name collection.controller.js
import Collection from '../models/collection.schema.js'
import tryCatchHandler from '../services/tryCatchHandler.js'
import CustomError from '../utils/customError.js'

/***************************************************************************************
 * @Create_COLLECTION
 * @route http://localhost:4000/api/collection
 * @description: FOR CREATING COLLECTION
 * @parameters 
 * @return COLLECTION
 ***************************************************************************************/

export const createCollection = tryCatchHandler(async (req, res)=>{
    // take name from frontend.
    const {name}= req.body

    if (!name){
        throw new CustomError("Collection name is required", 400)
    }

    //add this name to database
     const collection = await Collection.create ({
        name
    })

    // send this response value to frontend
    res.status(200).json({
        success:true,
        message: "Collection created with success",
        collection
    })
})


/***************************************************************************************
 * @UPDATE_COLLECTION
 * @route http://localhost:4000/api/collection
 * @description: FOR EDITING / UPDATING COLLECTION
 * @parameters 
 * @return COLLECTION
 ***************************************************************************************/
export const updateCollection = tryCatchHandler(async (req, res) =>{
    // existing value to be updates
    const {id:CollectionId} = req.params
    // new value to get updated
    const {name}= req.body

    if(!name){
        throw new CustomError("Collection name is required", 400)
    }

    let updatedCollection = await Collection.findByIdAndUpdate(
        CollectionId,
        {
            name
        },
        {
            new:true,
            runValidators:true
        }
    )

    if(!updatedCollection){
        throw new CustomError("Collection not found", 400)
    }

    // send response to frontend
    res.status(200).json({
        success:true,
        message:"Collection updated successfully",
        updatedCollection

    })
})

/***************************************************************************************
 * @DELETE_COLLECTION
 * @route http://localhost:4000/api/collection
 * @description: FOR DELETING COLLECTION
 * @parameters 
 * @return COLLECTION
 ***************************************************************************************/

export const deleteCollection = tryCatchHandler(async (req, res)=>{
    const {id:collectionId}= req.params

    const collectionToDelete= await Collection.findOneAndDelete(collectionId);

    if(!collectionToDelete){
        throw new CustomError("Collection not found", 400)
    }
        collectionToDelete.deleteOne()
    // send response to frontend
    res.status(200).json({
        success:true,
        message: "collection deleted sucessfully",
        // collectionToDelete1
    })
})

/***************************************************************************************
 * @GET_ALL_COLLECTION
 * @route http://localhost:4000/api/collection
 * @description: TO READ ALL THE COLLECTION
 * @parameters 
 * @return COLLECTION
 ***************************************************************************************/
export const getAllCollections = tryCatchHandler(async ( req, res)=>{
    const collections = await Collection.find()

    if(!collections){
        throw new CustomError("No Collection found", 400)
    }

    res.status(200).json({
        success:true,
        collections
    })
})
  1. Create a file name product.controller.js
import Product from '../models/product.schema.js'
import tryCatchHandler from '../services/tryCatchHandler.js'
import WhereClause from '../utils/whereClause.js';
import CustomError from '../utils/customError.js';
import cloudinary  from "cloudinary";


// All User

export const getAllProduct = tryCatchHandler(async (req, res, next) => {
    const resultPerPage = 6;
    const totalcountProduct = await Product.countDocuments();

    const productsObj = new WhereClause(Product.find(), req.query)
      .search()
      .filter();

    let products = await productsObj.base;
    const filteredProductNumber = products.length;

    //products.limit().skip()

    productsObj.pager(resultPerPage);
    products = await productsObj.base.clone();

    res.status(200).json({
      success: true,
      products,
      filteredProductNumber,
      totalcountProduct,
    });
  });

export const getOneProduct= tryCatchHandler(async(req,res, next) =>{
    const product = await Product.findById(req.params.id)

    if(!product){
        return next(new CustomError('No product found with this id', 400))
    }

    res.status(200).json({
        success:true,
        product

    })
})

// admin only
export const createProduct = tryCatchHandler(async(req, res, next)=>{

    // images
    let imageArray =[];

    if(!req.files){
        return next(new CustomError('images are required', 400))
    }

    if (req.files){
        for(let i=0; i< req.files.photos.length; i++){
        // const element = req.files.photos[i];
        // cloudinary
            let result = await cloudinary.v2.uploader.upload(req.files.photos[i].
            tempFilePath, {
                folder: 'products'
            }
            );

            imageArray.push({
                id: result.public_id,
                secure_url:result.secure_url
            })
        }
    }

    req.body.photos = imageArray;
    req.body.user = req.user.id

    const product =await Product.create(req.body)
    res.status(200).json({
        success:true,
        product,
    })
});

export const adminGetAllProduct = tryCatchHandler(async (req, res , next)=>{
    const products = await Product.find()


    res.status(200).json({
        success:true,
        products

    })
})

export const adminUpdateOneProduct = tryCatchHandler(async(req, res, next)=>{
    let product = await  Product.findById(req.params.id);

    if (!product){
        return next (new CustomError("No product found with this id", 400))
    }

    let imagesArray =[]

    if(req.files){
        // destroy the existing image

        for (let i = 0; i < product.photos.length; i++) {
            const res = await cloudinary.v2.uploader.destroy(product.photos[i].id)

        }
        // upload and save the images

        for(let i=0; i< req.files.photos.length; i++){
            // const element = req.files.photos[i];
            // cloudinary
                let result = await cloudinary.v2.uploader.upload(req.files.photos[i].
                tempFilePath, {
                    folder: 'products',
                }
                );

                imagesArray.push({
                    id: result.public_id,
                    secure_url:result.secure_url
                })
            }
    }

    req.body.photos = imagesArray




    product = await Product.findByIdAndUpdate(req.params.id, req.body,{
        new: true,
        runValidators: true,
        useFindAndModify: false
    })

    res.status(200).json({
        success: true,
        product,
    })
})

export const adminDeleteOneProduct = tryCatchHandler(async(req, res, next)=>{
    const product = await  Product.findById(req.params.id);

    if (!product){
        return next (new CustomError("No product found with this id", 400))
    }

    // destroy the existing image.
    for (let i = 0; i < product.photos.length; i++) {
        const res = await cloudinary.uploader.destroy(product.photos[i].id)

    }

   await product.deleteOne()

    res.status(200).json({
        success: true,
        message: "Product was deleted !"
    })
})
  1. Create a file name order.controller.js
import Order from '../models/order.schema.js'
import tryCatchHandler from '../services/tryCatchHandler.js'
import CustomError from '../utils/customError.js'
import Product from '../models/product.schema.js'

export const createOrder = tryCatchHandler(async (req, res, next) => {
  const {
    shippingInfo,
    orderItems,
    paymentInfo,
    taxAmount,
    shippingAmount,
    totalAmount,
  } = req.body;

  const order = await Order.create({
    shippingInfo,
    orderItems,
    paymentInfo,
    taxAmount,
    shippingAmount,
    totalAmount,
    user: req.user._id,
  });

  res.status(200).json({
    success: true,
    order,
  });
});

export const getOneOrder = tryCatchHandler(async (req, res, next) => {
  const order = await Order.findById(req.params.id).populate(
    "user",
    "name email"
  );

  if (!order) {
    return next(new CustomError("please check order id", 401));
  }

  res.status(200).json({
    success: true,
    order,
  });
});

export const getLoggedInOrders = tryCatchHandler(async (req, res, next) => {
  const order = await Order.find({ user: req.user._id });

  if (!order) {
    return next(new CustomError("please check order id", 401));
  }

  res.status(200).json({
    success: true,
    order,
  });
});

export const admingetAllOrders = tryCatchHandler(async (req, res, next) => {
  const orders = await Order.find();

  res.status(200).json({
    success: true,
    orders,
  });
});

export const adminUpdateOrder = tryCatchHandler(async (req, res, next) => {
  const order = await Order.findById(req.params.id);

  if (order.orderStatus === "Delivered") {
    return next(new CustomError("Order is already marked for delivered", 401));
  }

  order.orderStatus = req.body.orderStatus;

  order.orderItems.forEach(async (prod) => {
    await updateProductStock(prod.product, prod.quantity);
  });

  await order.save();

  res.status(200).json({
    success: true,
    order,
  });
});

export const adminDeleteOrder = tryCatchHandler(async (req, res, next) => {
  const order = await Order.findById(req.params.id);

  await order.deleteOne();

  res.status(200).json({
    success: true,
  });
});

async function updateProductStock(productId, quantity) {
  const product = await Product.findById(productId);

  product.stock = product.stock - quantity;

  await product.save({ validateBeforeSave: false });
}

Routes

Create a folder name routes. Inside it create the file as below.

  1. Create a file name auth.js
import express from 'express'
const router = express.Router();

import { signUp, login, logout, getProfile, adminAllUser, admingetOneUser,adminUpdateOneUserDetails,adminDeleteOneUser} from '../controllers/auth.controller.js'

import { isLoggedIn, customRole } from '../middlewares/auth.middleware.js';
import AuthRoles from '../utils/authRoles.js';

router.post('/signup', signUp)
router.post('/login', login)
router.get('/logout', logout)
router.get('/profile', isLoggedIn, getProfile)



//admin only routes
router.route("/admin/users").get(isLoggedIn, customRole(AuthRoles.ADMIN), adminAllUser);

router
  .route("/admin/user/:id")
  .get(isLoggedIn, customRole(AuthRoles.ADMIN), admingetOneUser)
  .put(isLoggedIn, customRole(AuthRoles.ADMIN), adminUpdateOneUserDetails)
  .delete(isLoggedIn, customRole(AuthRoles.ADMIN), adminDeleteOneUser);





export default router
  1. Create a file name product.js
import express from "express";

import { createProduct,getAllProduct,getOneProduct,adminGetAllProduct, adminUpdateOneProduct , adminDeleteOneProduct } from "../controllers/product.controller.js";

const router =express.Router();

import { customRole, isLoggedIn } from '../middlewares/auth.middleware.js';
import AuthRoles from "../utils/authRoles.js";

// user routes
router.route('/products').get(getAllProduct)
router.route("/product/:id").get(getOneProduct);

// admin
router.post('/admin/product/add',isLoggedIn,customRole(AuthRoles.ADMIN),createProduct)

router.get('/admin/products', isLoggedIn,customRole(AuthRoles.ADMIN),adminGetAllProduct)

router
  .route("/admin/product/:id")
  .put(isLoggedIn, customRole(AuthRoles.ADMIN), adminUpdateOneProduct)
  .delete(isLoggedIn, customRole(AuthRoles.ADMIN), adminDeleteOneProduct);


export default router
  1. Create a file name collection.js
import express from 'express'

const router = express.Router();

// import AuthRoles from '../utils/authRoles.js';
import { createCollection, updateCollection, deleteCollection, getAllCollections } from '../controllers/collection.controller.js';
// import { customRole, isLoggedIn } from '../middlewares/auth.middleware.js';

router.route('/admin/collection')
    .post(createCollection)


router.route('/admin/collection/:id')
    .put(updateCollection)
    .delete(deleteCollection)

router.route('/admin/collections')
    .get(getAllCollections)

export default router
  1. Create a folder name order.js
import express from 'express'
import{
  createOrder,
  getOneOrder,
  getLoggedInOrders,
  admingetAllOrders,
  adminUpdateOrder,
  adminDeleteOrder,
} from "../controllers/order.controller.js"
const router = express.Router();
import { isLoggedIn, customRole } from "../middlewares/auth.middleware.js"
import AuthRoles from '../utils/authRoles.js';

router.route("/order/create").post(isLoggedIn, createOrder);
router.route("/order/:id").get(isLoggedIn, getOneOrder);
router.route("/myorder").get(isLoggedIn, getLoggedInOrders);

//admin routes
router
  .route("/admin/orders")
  .get(isLoggedIn, customRole(AuthRoles.ADMIN), admingetAllOrders);
router
  .route("/admin/order/:id")
  .put(isLoggedIn, customRole(AuthRoles.ADMIN), adminUpdateOrder)
  .delete(isLoggedIn, customRole(AuthRoles.ADMIN), adminDeleteOrder);

export default router

App.js

create a file name app.js inside root/server folder.

import express from 'express'
import cookieParser from 'cookie-parser'
import cors from 'cors'
import morgan from 'morgan'

import cloudinary from 'cloudinary'
import config from './config/index.js'
import fileUpload from 'express-fileupload'

 cloudinary.config({
    cloud_name: config.CLOUDINARY_NAME,
    api_key: config.CLOUDINARY_API_KEY,
    api_secret: config.CLOUDINARY_API_SECRET,
  })


const app = express()



app.use(express.json())
app.use(express.urlencoded({extended:true}))
app.use(cors())
app.use(cookieParser())


// Morgon logger
app.use(morgan('tiny'))

// Note that this option available for versions 1.0.0 and newer. 
app.use(fileUpload({
    useTempFiles : true,
    tempFileDir : '/tmp/'
}));

// import all routes here 
import  home from './routes/home.js'
import auth from './routes/auth.js'
import product from './routes/product.js'
import collection from './routes/collection.js'
import order from './routes/order.js'



// router middleware
app.use('/api/v1/', home)
app.use('/api/v1/', auth )
app.use('/api/v1/', product )
app.use('/api/v1/', collection )
app.use('/api/v1/', order)


export default app

Index.js

create a file name index.js inside root/ server folder.

import mongoose from 'mongoose'
import app from './app.js'
import config from "./config/index.js"


// create a fn
//  run a fn
// (async () =>{}) ()

(async ()=>{
    try {
        await mongoose.connect(config.MONGODB_URL)
        console.log("DB CONNECTED");

        app.on('error', (err)=>  {
            console.log("Error: ", err);
            throw err;
        })

        const onListening = () =>{
            console.log(`Listening on ${config.PORT}`);
        }

        app.listen(config.PORT, onListening)

    } catch (error) {
        console.log("ERROR", error);
        throw error
    }
}) ()

.env

Create a file .env inside the root directory.

JWT_SECRET= 
JWT_EXPIRY=
MONGODB_URL = 
PORT=

SMTP_MAIL_HOST = 
SMTP_MAIL_PORT=
SMTP_MAIL_USERNAME=
SMTP_MAIL_PASSWORD=
SMTP_MAIL_EMAIL=


CLOUDINARY_NAME =
CLOUDINARY_API_KEY=
CLOUDINARY_API_SECRET=

//  Give the your own value.

This is the whole process of making ecommerce backend with Node js.