In this article, you will learn about wexCommerce, an eCommerce Platform on Next.js.
Table of Contents
- Introduction
- Prerequisites
- Quick Overview
- Frontend
- Backend
- Background
- Installation
- Demo Database
- Run from Source
- Using the Code
- API
- Frontend
- Backend
- Points of Interest
- History

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.
- Node.js
- Express
- MongoDB
- Next.js
- React
- MUI
- JavaScript
- Git
In this section, you'll see a quick overview of the main pages of the frontend, the backend and the mobile app.
From the frontend, the user can search for available products, add products to cart and checkout.
Below is the main page of the frontend:

Below is a sample product page:

Below is a fullscreen view of product images:

Below is cart page:

Below is checkout page:

Below is the sign in page:

Below is the sign up page:

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

That's it! Those are the main pages of the frontend.
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:

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

Below is the page from which admins manage categories:

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

Below is the page from which admins edit products:

Below is a fullscreen view of product images:

Below is backend settings page:

That's it. Those are the main pages of the backend.
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.
Below are the installation instructions on Ubuntu Linux.
Prerequisites
- Install git, Node.js, NGINX, 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:
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
- Clone wexCommerce repo:
cd /opt
sudo git clone https://github.com/aelassas/wexcommerce.git
- Add permissions:
sudo chown -R $USER:$USER /opt/wexcommerce
sudo chmod -R +x /opt/wexcommerce/__scripts
- Create deployment shortcut:
sudo ln -s /opt/wexcommerce/__scripts/wc-deploy.sh /usr/local/bin/wc-deploy
- 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
- 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
- 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
- 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
- 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
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
- 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.
- Create an admin user by navigating to hostname:3000/sign-up
- Open backend/pages/sign-up.js and uncomment this line to secure the backend:
if (process.env.NODE_ENV === 'production') return { notFound: true };
- Redeploy wexCommerce backend:
wc-deploy backend
You can change language and currency from settings page from the backend.
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
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
- Download
wexCommerce
source code down to your machine. - 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
- 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
- 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
- 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;
}
}
- Create an admin user from http://localhost:8002/sign-up
You can change language and currency from settings page in the backend.

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.
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.

- ./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:
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:
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:
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:
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:
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:
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:
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:
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
:
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:
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:
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:
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.
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.

- ./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.
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.

- ./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.
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.
- 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