Click here to Skip to main content
15,867,453 members
Articles / Web Development / Node.js

wexCommerce - Open Source eCommerce Platform on Next.js

Rate me:
Please Sign up or sign in to vote.
4.76/5 (10 votes)
24 Jul 2023MIT11 min read 19.2K   417   34  
Open Source eCommerce Platform on Next.js
In this article, you will learn about wexCommerce, an eCommerce Platform on Next.js.

Table of Contents

  1. Introduction
  2. Prerequisites
  3. Quick Overview
    1. Frontend
    2. Backend
  4. Background
  5. Installation
  6. Demo Database
  7. Run from Source
  8. Using the Code
    1. API
    2. Frontend
    3. Backend
  9. Points of Interest
  10. History

Introduction

Image 1

A lot of people asked me to make simple eCommerce websites for them. I wanted to create a custom code base fully customizable as a starting point. So, I decided to create this eCommerce platform to make the development of eCommerce websites easy, simple and straightforward. And especially, to save time.

I focused on SEO because products need to be optmized for SEO and thus be indexed by search engines. That's why I chose Next.js and server side rendering. Single-Page Applications are fast but known to be not optimzed for SEO. So If you want to get a React app optimized for SEO, you should use Next.js. Otherwise, if SEO is not important like for backend apps, you can use pure React apps.

wexCommerce is an eCommerce platform on Next.js optimized for SEO with multiple interesting features. wexCommerce is optimized for SEO so that products can be indexed by search engines.

wexCommerce provides the following features:

  • Stock management
  • Order management
  • Client management
  • Multiple payment methods (Credit Card, Cash On Delivery, Wire Transfer)
  • Multiple delivery methods (Home delivery, Store withdrawal)
  • Multiple language support (English, French)
  • Responsive backend and frontend

In this article, you will learn how wexCommerce was made including a description of the main parts of the source code and the software architecture, how to deploy it, and how to run the source code. But before we dig in, we'll start with a quick overview of the platform.

Prerequisites

  • Node.js
  • Express
  • MongoDB
  • Next.js
  • React
  • MUI
  • JavaScript
  • Git

Quick Overview

In this section, you'll see a quick overview of the main pages of the frontend, the backend and the mobile app.

Frontend

From the frontend, the user can search for available products, add products to cart and checkout.

Below is the main page of the frontend:

Image 2

Below is a sample product page:

Image 3

Below is a fullscreen view of product images:

Image 4

Below is cart page:

Image 5

Below is checkout page:

Image 6

Below is the sign in page:

Image 7

Below is the sign up page:

Image 8

Below is the page where the user can see his orders:

Image 9

That's it! Those are the main pages of the frontend.

Backend

From the backend, admins can manage categories, products, users and orders.

Admins can also manage the following settings:

  • Locale Settings: Language of the platform (English or French) and currency
  • Delivery Settings: Delivery methods enabled and the cost of each one
  • Payment Settings: Payment methods enabled (Credit card, Cash on delivery or Wire transfer)
  • Bank Settings: Bank information for wire transfer (IBAN and other info)

Below is the sign in page of the backend:

Image 10

Below is the dashboard page of the backend from which admins can see and manage orders:

Image 11

Below is the page from which admins manage categories:

Image 12

Below is the page from which admins can see and manage products:

Image 13

Below is the page from which admins edit products:

Image 14

Below is a fullscreen view of product images:

Image 15

Below is backend settings page:

Image 16

That's it. Those are the main pages of the backend.

Background

The basic idea behind wexCommerce is very simple:

  • A backend: From which admins create and manage categories and products. A dashboard from which admins can see new orders and get notified by email on a new order.
  • A frontend: From which users can search for available products, add them to cart and then checkout with multiple payment methods and multiple delivery methods.

The backend and the frontend rely on wexCommerce API which is a RESTful API that exposes functions to access wexCommerce database.

Installation

Below are the installation instructions on Ubuntu Linux.

Prerequisites

  1. Install git, Node.js, NGINX, MongoDB and mongosh.
  2. Configure MongoDB:
    mongosh

    Create admin user:

    db = db.getSiblingDB('admin')
    db.createUser({ user: "admin" , pwd: "PASSWORD",
    roles: ["userAdminAnyDatabase", "dbAdminAnyDatabase", "readWriteAnyDatabase"]})

    Replace PASSWORD with a strong password.

    Secure MongoDB:

    sudo nano /etc/mongod.conf

    Change configuration as follows:

    net:
      port: 27017
      bindIp: 0.0.0.0
    
    security:
      authorization: enabled

    Restart MongoDB service:

    sudo systemctl restart mongod.service
    sudo systemctl status mongod.service

Instructions

  1. Clone wexCommerce repo:
    cd /opt
    sudo git clone https://github.com/aelassas/wexcommerce.git
  2. Add permissions:
    sudo chown -R $USER:$USER /opt/wexcommerce
    sudo chmod -R +x /opt/wexcommerce/__scripts
  3. Create deployment shortcut:
    sudo ln -s /opt/wexcommerce/__scripts/wc-deploy.sh /usr/local/bin/wc-deploy
  4. Create wexCommerce services:
    sudo cp /opt/wexcommerce/__services/wexcommerce.service /etc/systemd/system
    sudo systemctl enable wexcommerce.service
    
    sudo cp /opt/wexcommerce/__services/wexcommerce-backend.service /etc/systemd/system
    sudo systemctl enable wexcommerce-backend.service
    
    sudo cp /opt/wexcommerce/__services/wexcommerce-frontend.service /etc/systemd/system
    sudo systemctl enable wexcommerce-frontend.service
  5. Add /opt/wexcommerce/api/.env file:
    NODE_ENV = production
    WC_PORT = 4004
    WC_HTTPS = false
    WC_PRIVATE_KEY = /etc/ssl/wexcommerce.key
    WC_CERTIFICATE = /etc/ssl/wexcommerce.crt
    WC_DB_HOST = 127.0.0.1
    WC_DB_PORT = 27017
    WC_DB_SSL = false
    WC_DB_SSL_KEY = /etc/ssl/wexcommerce.key
    WC_DB_SSL_CERT = /etc/ssl/wexcommerce.crt
    WC_DB_SSL_CA = /etc/ssl/wexcommerce.ca.pem
    WC_DB_DEBUG = true
    WC_DB_APP_NAME = wexcommerce
    WC_DB_AUTH_SOURCE = admin
    WC_DB_USERNAME = admin
    WC_DB_PASSWORD = PASSWORD
    WC_DB_NAME = wexcommerce
    WC_JWT_SECRET = PASSWORD
    WC_JWT_EXPIRE_AT = 86400
    WC_TOKEN_EXPIRE_AT = 86400
    WC_SMTP_HOST = in-v3iljet.com
    WC_SMTP_PORT = 587
    WC_SMTP_USER = USER
    WC_SMTP_PASS = PASSWORD
    WC_SMTP_FROM = admin@wexcommerce.com
    WC_ADMIN_EMAIL = admin@wexcommerce.com
    WC_CDN_PRODUCTS = /var/www/cdn/wexcommerce/products
    WC_CDN_TEMP_PRODUCTS =  /var/www/cdn/wexcommerce/temp/products
    WC_BACKEND_HOST = http://localhost:8002/
    WC_FRONTEND_HOST = http://localhost:8001/
    WC_DEFAULT_LANGUAGE = en
    WC_DEFAULT_CURRENCY = $ 

    You must configure the following options:

    WC_DB_PASSWORD
    WC_SMTP_USER
    WC_SMTP_PASS
    WC_SMTP_FROM
    WC_ADMIN_EMAIL
    WC_BACKEND_HOST
    WC_FRONTEND_HOST 

    If you want to enable SSL, You must configure the following options:

    WC_HTTPS = true
    WC_PRIVATE_KEY
    WC_CERTIFICATE 
  6. Add /opt/wexcommerce/backend/.env file:
    NEXT_PUBLIC_WC_API_HOST = http://localhost:4004
    NEXT_PUBLIC_WC_PAGE_SIZE = 30
    NEXT_PUBLIC_WC_CDN_PRODUCTS = http://localhost/cdn/wexcommerce/products
    NEXT_PUBLIC_WC_CDN_TEMP_PRODUCTS = http://localhost/cdn/wexcommerce/temp/products
    NEXT_PUBLIC_WC_APP_TYPE = backend 

    You must configure the following options:

    NEXT_PUBLIC_WC_API_HOST
    NEXT_PUBLIC_WC_CDN_PRODUCTS
    NEXT_PUBLIC_WC_CDN_TEMP_PRODUCTS 
  7. Add /opt/wexcommerce/frontend/.env file:
    NEXT_PUBLIC_WC_API_HOST = http://localhost:4004
    NEXT_PUBLIC_WC_PAGE_SIZE = 30
    NEXT_PUBLIC_WC_CDN_PRODUCTS = http://localhost/cdn/wexcommerce/products
    NEXT_PUBLIC_WC_CDN_TEMP_PRODUCTS = http://localhost/cdn/wexcommerce/temp/products
    NEXT_PUBLIC_WC_APP_TYPE = frontend

    You must configure the following options:

    NEXT_PUBLIC_WC_API_HOST
    NEXT_PUBLIC_WC_CDN_PRODUCTS
    NEXT_PUBLIC_WC_CDN_TEMP_PRODUCTS
  8. Configure NGINX:
    sudo nano /etc/nginx/sites-available/default

    Change the configuration as follows for the frontend (NGINX reverse proxy):

    server {
        #listen 443 http2 ssl default_server;
        listen 80 default_server;
        server_name _;
    
        #ssl_certificate_key /etc/ssl/wexcommerce.key;
        #ssl_certificate /etc/ssl/wexcommerce.pem;
    
        access_log /var/log/nginx/wexcommerce.frontend.access.log;
        error_log /var/log/nginx/wexcommerce.frontend.error.log;
    
        location / {
          # reverse proxy for next server
          proxy_pass http://localhost:8001;
          proxy_http_version 1.1;
          proxy_set_header Upgrade $http_upgrade;
          proxy_set_header Connection 'upgrade';
          proxy_set_header Host $host;
          proxy_cache_bypass $http_upgrade;
        }
    
        location /cdn {
          alias /var/www/cdn;
        }
    }

    If you want to enable SSL, uncomment these lines:

    #listen 443 http2 ssl default_server;
    #ssl_certificate_key /etc/ssl/wexcommerce.key;
    #ssl_certificate /etc/ssl/wexcommerce.pem;

    Change the configuration as follows for the backend (NGINX reverse proxy):

    server {
        #listen 3000 http2 ssl default_server;
        listen 3000 default_server;
        server_name _;
    
        #ssl_certificate_key /etc/ssl/wexcommerce.key;
        #ssl_certificate /etc/ssl/wexcommerce.pem;
    
        #error_page 497 301 =307 https://$host:$server_port$request_uri;
    
        access_log /var/log/nginx/wexcommerce.backend.access.log;
        error_log /var/log/nginx/wexcommerce.backend.error.log;
    
        location / {
          # reverse proxy for next server
          proxy_pass http://localhost:8002;
          proxy_http_version 1.1;
          proxy_set_header Upgrade $http_upgrade;
          proxy_set_header Connection 'upgrade';
          proxy_set_header Host $host;
          proxy_cache_bypass $http_upgrade;
        }
    }

    If you want to enable SSL, uncomment these lines:

    #listen 3000 http2 ssl default_server;
    #ssl_certificate_key /etc/ssl/wexcommerce.key;
    #ssl_certificate /etc/ssl/wexcommerce.pem;
    #error_page 497 301 =307 https://$host:$server_port$request_uri;

    Then, check nginx configuration and start nginx service:

    sudo nginx -t
    sudo systemctl restart nginx.service
    sudo systemctl status nginx.service
  9. enable firewall and open wexCommerce ports:
    sudo ufw enable
    sudo ufw allow 4004/tcp
    sudo ufw allow 80/tcp
    sudo ufw allow 443/tcp
    sudo ufw allow 3000/tcp
  10. Deploy wexCommerce:
    wc-deploy all

    wexCommerce backend is accessible on port 3000 and the frontend is accessible on port 80 or 443 if SSL is enabled.

  11. Create an admin user by navigating to hostname:3000/sign-up
  12. Open backend/pages/sign-up.js and uncomment this line to secure the backend:
    JavaScript
    if (process.env.NODE_ENV === 'production') return { notFound: true };
  13. Redeploy wexCommerce backend:
    wc-deploy backend

    You can change language and currency from settings page from the backend.

Demo Database

Download wexcommerce-db.zip down to your machine.

Restore wexCommerc demo database by using the following command:

mongorestore --verbose --drop --gzip --host=127.0.0.1
--port=27017 --username=admin --password=$PASSWORD
--authenticationDatabase=admin --nsInclude="wexcommerce.*" --archive=wexcommerce.gz

Don't forget to set $PASSWORD.

Unzip cdn.zip on your web server so that the files will be accessible through http://localhost/cdn/wexcommerce/.

cdn/wexcommerce/ contains the following folders:

  • cdn/wexcommerce/products: This folder contains products images.
  • cdn/wexcommerce/temp: This folder contains temporary files.

Admin user: admin@wexcommerce.com
Password: sh0ppingC4rt

Run from Source

Below are the instructions to run wexCommerce from code.

Prerequisites

Install git, Node.js, NGINX on Linux or IIS on Windows, MongoDB and mongosh.

Configure MongoDB:

mongosh

Create admin user:

db = db.getSiblingDB('admin')
db.createUser({ user: "admin" , pwd: "PASSWORD",
roles: ["userAdminAnyDatabase", "dbAdminAnyDatabase", "readWriteAnyDatabase"]})

Replace PASSWORD with a strong password.

Secure MongoDB by changing mongod.conf as follows:

net:
  port: 27017
  bindIp: 0.0.0.0

security:
  authorization: enabled

Restart MongoDB service.

Instructions

  1. Download wexCommerce source code down to your machine.
  2. Add api/.env file:
    NODE_ENV = development
    WC_PORT = 4004
    WC_HTTPS = false
    WC_PRIVATE_KEY = /etc/ssl/wexcommerce.key
    WC_CERTIFICATE = /etc/ssl/wexcommerce.crt
    WC_DB_HOST = 127.0.0.1
    WC_DB_PORT = 27017
    WC_DB_SSL = false
    WC_DB_SSL_KEY = /etc/ssl/wexcommerce.key
    WC_DB_SSL_CERT = /etc/ssl/wexcommerce.crt
    WC_DB_SSL_CA = /etc/ssl/wexcommerce.ca.pem
    WC_DB_DEBUG = true
    WC_DB_APP_NAME = wexcommerce
    WC_DB_AUTH_SOURCE = admin
    WC_DB_USERNAME = admin
    WC_DB_PASSWORD = PASSWORD
    WC_DB_NAME = wexcommerce
    WC_JWT_SECRET = PASSWORD
    WC_JWT_EXPIRE_AT = 86400
    WC_TOKEN_EXPIRE_AT = 86400
    WC_SMTP_HOST = in-v3iljet.com
    WC_SMTP_PORT = 587
    WC_SMTP_USER = USER
    WC_SMTP_PASS = PASSWORD
    WC_SMTP_FROM = admin@wexcommerce.com
    WC_ADMIN_EMAIL = admin@wexcommerce.com
    WC_CDN_PRODUCTS = /var/www/cdn/wexcommerce/products
    WC_CDN_TEMP_PRODUCTS =  /var/www/cdn/wexcommerce/temp/products
    WC_BACKEND_HOST = http://localhost:8002/
    WC_FRONTEND_HOST = http://localhost:8001/
    WC_DEFAULT_LANGUAGE = en
    WC_DEFAULT_CURRENCY = $

    You must configure the following options:

    WC_DB_PASSWORD
    WC_SMTP_USER
    WC_SMTP_PASS
    WC_SMTP_FROM
    WC_ADMIN_EMAIL
    WC_BACKEND_HOST
    WC_FRONTEND_HOST

    Install nodemon:

    npm i -g nodemon

    Run api:

    cd ./api
    npm install
    npm run dev
  3. Add backend/.env file:
    NEXT_PUBLIC_WC_API_HOST = http://localhost:4004
    NEXT_PUBLIC_WC_PAGE_SIZE = 30
    NEXT_PUBLIC_WC_CDN_PRODUCTS = http://localhost/cdn/wexcommerce/products
    NEXT_PUBLIC_WC_CDN_TEMP_PRODUCTS = http://localhost/cdn/wexcommerce/temp/products
    NEXT_PUBLIC_WC_APP_TYPE = backend

    You must configure the following options:

    NEXT_PUBLIC_WC_API_HOST
    NEXT_PUBLIC_WC_CDN_PRODUCTS
    NEXT_PUBLIC_WC_CDN_TEMP_PRODUCTS

    Run backend:

    cd ./backend
    npm install
    npm run dev
  4. Add frontend/.env file:
    NEXT_PUBLIC_WC_API_HOST = http://localhost:4004
    NEXT_PUBLIC_WC_PAGE_SIZE = 30
    NEXT_PUBLIC_WC_CDN_PRODUCTS = http://localhost/cdn/wexcommerce/products
    NEXT_PUBLIC_WC_CDN_TEMP_PRODUCTS = http://localhost/cdn/wexcommerce/temp/products
    NEXT_PUBLIC_WC_APP_TYPE = frontend

    You must configure the following options:

    NEXT_PUBLIC_WC_API_HOST
    NEXT_PUBLIC_WC_CDN_PRODUCTS
    NEXT_PUBLIC_WC_CDN_TEMP_PRODUCTS

    Run frontend:

    cd ./frontend
    npm install
    npm run dev
  5. Configure http://localhost/cdn
    • On Windows, install IIS and create C:\inetpub\wwwroot\cdn folder.
    • On Linux, install NGINX and add cdn folder by changing /etc/nginx/sites-available/default as follows:
    server {
        listen 80 default_server;
        server_name _;
    
        ...
    
        location /cdn {
          alias /var/www/cdn;
        }
    }
  6. Create an admin user from http://localhost:8002/sign-up

You can change language and currency from settings page in the backend.

Using the Code

Image 17

This section describes the software architecture of wexCommerce including the API, the frontend and the backend.

wexCommerce API is a Node.js server application that exposes a RESTful API using Express which gives access to wexCommerce MongoDB database.

wexCommerce frontend is a Next.js web application that is the main web interface for ordering products.

wexCommerce backend is a Next.js web application that lets admins manage categories, products, orders and users.

API

wexCommerce API exposes all wexCommerce functions needed for the backend and the frontend. The API follows the MVC design pattern. JWT is used for authentication. There are some functions that need authentication such as functions related to managing products and orders, and others that do not need authentication such as retrieving categories and available products for non authenticated users.

Image 18

  • ./api/models/ folder contains MongoDB models.
  • ./api/routes/ folder contains Express routes.
  • ./api/controllers/ folder contains controllers.
  • ./api/middlewares/ folder contains middlewares.
  • ./api/server.js is the main server where database connection is established and routes are loaded.
  • ./api/app.js is the main entry point of wexCommerce API.

app.js

app.js is the main entry point of wexCommerce API:

JavaScript
import app from './server.js'
import fs from 'fs'
import https from 'https'

const PORT = parseInt(process.env.WC_PORT) || 4000
const HTTPS = process.env.WC_HTTPS.toLocaleLowerCase() === 'true'
const PRIVATE_KEY = process.env.WC_PRIVATE_KEY
const CERTIFICATE = process.env.WC_CERTIFICATE

if (HTTPS) {
    https.globalAgent.maxSockets = Infinity
    const privateKey = fs.readFileSync(PRIVATE_KEY, 'utf8')
    const certificate = fs.readFileSync(CERTIFICATE, 'utf8')
    const credentials = { key: privateKey, cert: certificate }
    const httpsServer = https.createServer(credentials, app)

    httpsServer.listen(PORT, async () => {
        console.log('HTTPS server is running on Port:', PORT)
    })
} else {
    app.listen(PORT, async () => {
        console.log('HTTP server is running on Port:', PORT)
    })
}

In app.js, we retrieve HTTPS setting variable which indicates if https is enabled or not. If https is enabled, we create an https server using the provided private key and certificate and start listening. Otherwise, an http server is created and we start listening. app is the main server where database connection is established and routes are loaded.

server.js

server.js is in the main server:

JavaScript
import express from 'express'
import cors from 'cors'
import mongoose from 'mongoose'
import compression from 'compression'
import helmet from 'helmet'
import nocache from 'nocache'
import strings from './config/app.config.js'
import userRoutes from './routes/userRoutes.js'
import categoryRoutes from './routes/categoryRoutes.js'
import productRoutes from './routes/productRoutes.js'
import cartRoutes from './routes/cartRoutes.js'
import orderRoutes from './routes/orderRoutes.js'
import notificationRoutes from './routes/notificationRoutes.js'
import deliveryTypeRoutes from './routes/deliveryTypeRoutes.js'
import paymentTypeRoutes from './routes/paymentTypeRoutes.js'
import settingRoutes from './routes/settingRoutes.js'
import * as deliveryTypeController from './controllers/deliveryTypeController.js'
import * as paymentTypeController from './controllers/paymentTypeController.js'
import * as settingController from './controllers/settingController.js'

const DB_HOST = process.env.WC_DB_HOST
const DB_PORT = process.env.WC_DB_PORT
const DB_SSL = process.env.WC_DB_SSL.toLowerCase() === 'true'
const DB_SSL_KEY = process.env.WC_DB_SSL_KEY
const DB_SSL_CERT = process.env.WC_DB_SSL_CERT
const DB_SSL_CA = process.env.WC_DB_SSL_CA
const DB_DEBUG = process.env.WC_DB_DEBUG.toLowerCase() === 'true'
const DB_AUTH_SOURCE = process.env.WC_DB_AUTH_SOURCE
const DB_USERNAME = process.env.WC_DB_USERNAME
const DB_PASSWORD = process.env.WC_DB_PASSWORD
const DB_APP_NAME = process.env.WC_DB_APP_NAME
const DB_NAME = process.env.WC_DB_NAME
const DB_URI = `mongodb://${encodeURIComponent(DB_USERNAME)}:${encodeURIComponent
//(DB_PASSWORD)}@${DB_HOST}:${DB_PORT}/${DB_NAME}?authSource=${DB_AUTH_SOURCE}&
//appName=${DB_APP_NAME}`

const init = async () => {
    let done = await deliveryTypeController.init()
    done &= await paymentTypeController.init()
    done &= await settingController.init()

    if (done) {
        console.log('Initialization succeeded')
    } else {
        console.log('Initialization failed')
    }
}

let options = {}
if (DB_SSL) {
    options = {
        ssl: true,
        sslValidate: true,
        sslKey: DB_SSL_KEY,
        sslCert: DB_SSL_CERT,
        sslCA: [DB_SSL_CA]
    }
}

mongoose.set('debug', DB_DEBUG)
mongoose.Promise = global.Promise
mongoose.connect(DB_URI, options)
    .then(
        async () => {
            console.log('Database is connected')
            await init()
        },
        (err) => {
            console.error('Cannot connect to the database:', err)
        }
    )

const app = express()
app.use(helmet.contentSecurityPolicy())
app.use(helmet.dnsPrefetchControl())
app.use(helmet.crossOriginEmbedderPolicy())
app.use(helmet.frameguard())
app.use(helmet.hidePoweredBy())
app.use(helmet.hsts())
app.use(helmet.ieNoOpen())
app.use(helmet.noSniff())
app.use(helmet.permittedCrossDomainPolicies())
app.use(helmet.referrerPolicy())
app.use(helmet.xssFilter())
app.use(helmet.originAgentCluster())
app.use(helmet.crossOriginResourcePolicy({ policy: 'cross-origin' }))
app.use(helmet.crossOriginOpenerPolicy())
app.use(nocache())
app.use(compression({ threshold: 0 }))
app.use(express.urlencoded({ limit: '50mb', extended: true }))
app.use(express.json({ limit: '50mb' }))
app.use(cors())
app.use('/', userRoutes)
app.use('/', categoryRoutes)
app.use('/', productRoutes)
app.use('/', cartRoutes)
app.use('/', orderRoutes)
app.use('/', notificationRoutes)
app.use('/', deliveryTypeRoutes)
app.use('/', paymentTypeRoutes)
app.use('/', settingRoutes)

strings.setLanguage(process.env.WC_DEFAULT_LANGUAGE)

export default app;

First of all, we build MongoDB connection string, then we establish a connection with BookCars MongoDB database. Then we create an Express app and load middlewares. Finally, we load Express routes and export app.

Routes

There are nine routes in wexCommerce API. Each route has its own controller following the MVC design pattern and SOLID principles. Below are the main routes:

  • userRoutes: Provides REST functions related to users
  • categoryRoutes: Provides REST functions related to categories
  • productRoutes: Provides REST functions related to products
  • cartRoutes: Provides REST functions related to carts
  • deliveryTypeRoutes: Provides REST functions related to delivery methods
  • paymentTypeRoutes: Provides REST functions related to payment methods
  • orderRoutes: Provides REST functions related to orders
  • notificationRoutes: Provides REST functions related to notifications
  • settingRoutes: Provides REST functions related to settings

We are not going to explain each route one by one. We'll take, for example, categoryRoutes and see how it was made:

JavaScript
import express from 'express'
import routeNames from '../config/categoryRoutes.config.js'
import authJwt from '../middlewares/authJwt.js'
import * as categoryController from '../controllers/categoryController.js'

const routes = express.Router()

routes.route(routeNames.validate).post
            (authJwt.verifyToken, categoryController.validate)
routes.route(routeNames.checkCategory).get
            (authJwt.verifyToken, categoryController.checkCategory)
routes.route(routeNames.create).post(authJwt.verifyToken, categoryController.create)
routes.route(routeNames.update).put(authJwt.verifyToken, categoryController.update)
routes.route(routeNames.delete).delete
            (authJwt.verifyToken, categoryController.deleteCategory)
routes.route(routeNames.getCategory).get
            (authJwt.verifyToken, categoryController.getCategory)
routes.route(routeNames.getCategories).get(categoryController.getCategories)
routes.route(routeNames.searchCategories).get
            (authJwt.verifyToken, categoryController.searchCategories)

export default routes;

First of all, we create an Express Router. Then, we create routes using its name, its method, middlewares and its controller.

routeNames contains categoryRoutes route names:

JavaScript
export default {
    validate: '/api/validate-category',
    checkCategory: '/api/check-category/:id',
    create: '/api/create-category',
    update: '/api/update-category/:id',
    delete: '/api/delete-category/:id',
    getCategory: '/api/category/:id/:language',
    getCategories: '/api/categories/:language',
    searchCategories: '/api/search-categories/:language'
};

categoryController contains the main business logic regarding categories. We are not going to see all the source code of the controller since it's quite large but we'll take create and getCategories controller functions for example.

Below is Category model:

JavaScript
import mongoose from 'mongoose'

const Schema = mongoose.Schema

const categorySchema = new Schema({
    values: {
        type: [Schema.Types.ObjectId],
        ref: 'Value',
        validate: (value) => Array.isArray(value) && value.length > 1
    }
}, {
    timestamps: true,
    strict: true,
    collection: 'Category'
})

const categoryModel = mongoose.model('Category', categorySchema)

categoryModel.on('index', (err) => {
    if (err) {
        console.error('Category index error: %s', err)
    } else {
        console.info('Category indexing complete')
    }
})

export default categoryModel;

A Category has multiple values. One value per language. By default, English and French languages are supported.

Below is Value model:

JavaScript
import mongoose from 'mongoose'

const Schema = mongoose.Schema

const valueSchema = new Schema({
    language: {
        type: String,
        required: [true, "can't be blank"],
        index: true,
        trim: true,
        lowercase: true,
        minLength: 2,
        maxLength: 2,
    },
    value: {
        type: String,
        required: [true, "can't be blank"],
        index: true,
        trim: true
    }
}, {
    timestamps: true,
    strict: true,
    collection: 'Value'
})

const valueModel = mongoose.model('Value', valueSchema)

valueModel.on('index', (err) => {
    if (err) {
        console.error('Value index error: %s', err)
    } else {
        console.info('Value indexing complete')
    }
})

export default valueModel;

A Value has a language code (ISO 639-1) and a string value.

Below is create controller function:

JavaScript
export const create = async (req, res) => {
    const values = req.body

    try {
        const _values = []
        for (let i = 0; i < values.length; i++) {
            const value = values[i]
            const _value = new Value({
                language: value.language,
                value: value.value
            })
            await _value.save()
            _values.push(_value._id)
        }

        const category = new Category({ values: _values })
        await category.save()
        return res.sendStatus(200)
    } catch (err) {
        console.error(`[category.create]  ${strings.DB_ERROR} ${req.body}`, err)
        return res.status(400).send(strings.DB_ERROR + err)
    }
};

In this function, we retrieve the body of the request, we iterate through the values provided in the body (one value per language) and we create a Value. Finally, we create the category depending on the created values.

Below is getCategories controller function:

JavaScript
export const getCategories = async (req, res) => {
    try {
        const language = req.params.language

        const categories = await Category.aggregate([
            {
                $lookup: {
                    from: 'Value',
                    let: { values: '$values' },
                    pipeline: [
                        {
                            $match: {
                                $and: [
                                    { $expr: { $in: ['$_id', '$$values'] } },
                                    { $expr: { $eq: ['$language', language] } }
                                ]
                            }
                        }
                    ],
                    as: 'value'
                }
            },
            { $unwind: { path: '$value', preserveNullAndEmptyArrays: false } },
            { $addFields: { name: '$value.value' } },
            { $project: { value: 0, values: 0 } },
            { $sort: { name: 1 } },
        ], { collation: { locale: Env.DEFAULT_LANGUAGE, strength: 2 } })

        return res.json(categories)
    } catch (err) {
        console.error(`[category.getCategories]  ${strings.DB_ERROR}`, err)
        return res.status(400).send(strings.DB_ERROR + err)
    }
};

In this controller function, we retrieve categories from database using aggregate MongoDB function depending on the language provided.

Below is another simple route, notificationRoutes:

JavaScript
import express from 'express'
import routeNames from '../config/notificationRoutes.config.js'
import authJwt from '../middlewares/authJwt.js'
import * as notificationController from '../controllers/notificationController.js'

const routes = express.Router()

routes.route(routeNames.notificationCounter).get(authJwt.verifyToken, notificationController.notificationCounter)
routes.route(routeNames.getNotifications).get(authJwt.verifyToken, notificationController.getNotifications)
routes.route(routeNames.markAsRead).post(authJwt.verifyToken, notificationController.markAsRead)
routes.route(routeNames.markAsUnRead).post(authJwt.verifyToken, notificationController.markAsUnRead)
routes.route(routeNames.delete).post(authJwt.verifyToken, notificationController.deleteNotifications)

export default routes

Below is Notification model:

JavaScript
import mongoose from 'mongoose'

const Schema = mongoose.Schema

const notificationSchema = new Schema({
    user: {
        type: Schema.Types.ObjectId,
        required: [true, "can't be blank"],
        ref: 'User',
        index: true
    },
    message: {
        type: String,
        required: [true, "can't be blank"]
    },
    isRead: {
        type: Boolean,
        default: false
    },
    order: {
        type: Schema.Types.ObjectId,
        ref: 'Order',
        index: true
    },
}, {
    timestamps: true,
    strict: true,
    collection: 'Notification'
})

const notificationModel = mongoose.model('Notification', notificationSchema)

notificationModel.on('index', (err) => {
    if (err) {
        console.error('Notification index error: %s', err)
    } else {
        console.info('Notification indexing complete')
    }
})

export default notificationModel

A Notification is composed of a reference to a user, a message, a reference to an order and isRead flag.

Below is getNotifications controller function:

JavaScript
export const getNotifications = async (req, res) => {
    try {
        const userId = mongoose.Types.ObjectId(req.params.userId)
        const page = parseInt(req.params.page)
        const size = parseInt(req.params.size)

        const notifications = await Notification.aggregate([
            { $match: { user: userId } },
            {
                $facet: {
                    resultData: [
                        { $sort: { createdAt: -1 } },
                        { $skip: ((page - 1) * size) },
                        { $limit: size },
                    ],
                    pageInfo: [
                        {
                            $count: 'totalRecords'
                        }
                    ]
                }
            }
        ])

        res.json(notifications)
    } catch (err) {
        console.error(strings.DB_ERROR, err)
        res.status(400).send(strings.DB_ERROR + err)
    }
};

In this simple controller function, we retrieve notifications using MongoDB aggregate function, page and size parameters.

Below is markAsRead controller function:

JavaScript
export const markAsRead = async (req, res) => {

    try {
        const { ids: _ids } = req.body, ids = _ids.map(id => mongoose.Types.ObjectId(id))
        const { userId: _userId } = req.params, userId = mongoose.Types.ObjectId(_userId)

        const bulk = Notification.collection.initializeOrderedBulkOp()
        const notifications = await Notification.find({ _id: { $in: ids } })

        bulk.find({ _id: { $in: ids }, isRead: false }).update({ $set: { isRead: true } })
        bulk.execute(async (err, response) => {
            if (err) {
                console.error(`[notification.markAsRead] ${strings.DB_ERROR}`, err)
                return res.status(400).send(strings.DB_ERROR + err)
            }

            const counter = await NotificationCounter.findOne({ user: userId })
            counter.count -= notifications.filter(notification => !notification.isRead).length
            await counter.save()

            return res.sendStatus(200)
        })

    } catch (err) {
        console.error(`[notification.markAsRead] ${strings.DB_ERROR}`, err)
        return res.status(400).send(strings.DB_ERROR + err)
    }
};

In this controller function we bulk update notifications and mark them as read.

Frontend

The frontend is a web application built with Node.js, Next.js, React and MUI. From the frontend, the user can search for available products, add them to cart and proceed to checkout depending on delivery and payment methods.

Image 19

  • ./frontend/public/ folder contains public assets.
  • ./frontend/styles/ folder contains CSS styles.
  • ./frontend/components/ folder contains React components.
  • ./frontend/lang contains locale files.
  • ./frontend/pages/ folder contains Next.js pages.
  • ./frontend/services/ contains wexCommerce API client services.
  • ./frontend/next.config.js is the main configuration file of the frontend.

The frontend was created with create-next-app:

npx create-next-app@latest

In Next.js, a page is a React Component exported from a .js, .jsx, .ts, or .tsx file in the pages directory. Each page is associated with a route based on its file name.

By default, Next.js pre-renders every page. This means that Next.js generates HTML for each page in advance, instead of having it all done by client-side JavaScript. Pre-rendering can result in better performance and SEO.

Each generated HTML is associated with minimal JavaScript code necessary for that page. When a page is loaded by the browser, its JavaScript code runs and makes the page fully interactive. (This process is called hydration.)

wexCommerce uses Server-side Rendering for SEO optimization so that products can be indexed by search engines.

Backend

The backend is a web application built with Node.js, Next.js, React and MUI. From the backend, admins can manage categories, products, orders and users. When a new order is created, the admin user gets a notification in the backend and receives an automatic email.

Image 20

  • ./backend/public/ folder contains public assets.
  • ./backend/styles/ folder contains CSS styles.
  • ./backend/components/ folder contains React components.
  • ./backend/lang contains locale files.
  • ./backend/pages/ folder contains Next.js pages.
  • ./backend/services/ contains wexCommerce API client services.
  • ./backend/next.config.js is the main configuration file of the backend.

The backend was created with create-next-app too:

npx create-next-app@latest

The backend does not need SEO optimization since it's designed for managing wexCommerce assets.

Points of Interest

Using the same language for both the backend and frontend development is very nice and simple.

That's it! I hope you enjoyed reading this article.

History

  • 10th November, 2022 - Initial release
  • 19th November, 2022 - Updates in source code and content
  • 26th November, 2022 - Updates in source code and content
  • 9th December, 2022 - Updates in source code and content
  • 9th February, 2023 - Updates in source code and content
  • 23rd July, 2023 - wexCommerce 1.1 released
    • Upgrade to Next.js 13.4.12
    • Added devDependencies
    • Updated fs/promises
    • Updated notificationController.js
    • Fixed localization issues
    • Bump dotenv from 16.0.3 to 16.3.1
    • Bump helmet from 6.0.1 to 7.0.0
    • Bump jsonwebtoken from 8.5.1 to 9.0.1
    • Bump mongoose from 6.8.0 to 7.4.0
    • Bump nocache from 3.0.4 to 4.0.0
    • Bump nodemailer from 6.8.0 to 6.9.4
    • Bump validator from 13.7.0 to 13.9.0
    • Bump @emotion/react from 11.10.5 to 11.11.1
    • Bump @emotion/styled from 11.10.5 to 11.11.0
    • Bump @mui/icons-material from 5.11.0 to 5.14.1
    • Bump @mui/material from 5.11.0 to 5.14.1
    • Bump @mui/x-date-pickers from 5.0.11 to 6.10.1
    • Bump axios from 1.2.1 to 1.4.0
    • Bump cookies-next from 2.1.1 to 2.1.2
    • Bump date-fns from 2.29.3 to 2.30.0
    • Bump eslint from 8.30.0 to 8.45.0
    • Bump eslint-config-next from 13.0.7 to 13.4.12
    • Bump next from 13.0.7 to 13.4.12
    • Bump react-toastify from 9.1.1 to 9.1.3
    • Bump validator from 13.7.0 to 13.9.0

License

This article, along with any associated source code and files, is licensed under The MIT License


Written By
Engineer
Morocco Morocco
I build innovative and scalable solutions for digital media. With several years of software engineering experience, I have a strong background in web, mobile and desktop development, as well as media asset management and digital asset management systems.

My strength lies in the development of innovative solutions and the ability to adapt them to different industries looking to streamline or automate their work process or data management.

I am passionate about learning new technologies and frameworks and applying them to solve complex and challenging problems. I am proficient in working with Node.js, React, React Native, TypeScript, C# and .NET among other languages and tools. My ultimate aim is to deliver high-quality software products that meet the requirements and expectations of our customers.

Open-source projects:

- Wexflow: .NET Workflow Engine and Automation Platform
- BookCars: Car Rental Platform with Mobile App
- Movin' In: Rental Property Management Platform with Mobile App
- Wexstream: Video Conferencing Platform
- wexCommerce: eCommerce Platform on Next.js

If you'd like to discuss any sort of opportunity, feel free to contact me through GitHub or LinkedIn.

Comments and Discussions

 
-- There are no messages in this forum --