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
npm init -y
install all the package that is mentioned above.
Models
Create folder name Models. Inside it create the file as below.
- 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)
- 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)
- 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);
- 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.
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
- 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
- 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.
- 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.
- 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
- 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.
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,
});
});
- 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
})
})
- 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 !"
})
})
- 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.
- 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
- 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
- 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
- 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.