Express.js - Routes and Router with EJS templating engine and MVC and MongoDB
This is example eshop. You can add products, edit products, add products to cart and create orders. All is stored in nosql MongoDb.
Use MongoDB Compass
for working with database.
-utils
|
|---db.js
-routes
|
|---admin.js
|---shop.js
-controllers
|
|---shop.js
-models
|
|---product.js
-views
|
|--product-add.js
|--product-edit.js
|--product.ejs
|--shop.ejs
|
|---includes
| |
| |---head.ejs
| |---nav.ejs
|
|---shop
|
|---cart.ejs
|---orders.ejs
-public
|
|---css
|
|---main.css
-app.js
models/product.js
const url = 'mongodb+srv://MONGODB_USER:MONGODB_PASSWORD@cluster0-gconm.mongodb.net/MONGODB_DATABASE?retryWrites=true&w=majority';
const mongodb = require('mongodb');
const MongoClient = mongodb.MongoClient;
let _db;
const mongoConnect = (callback) => {
MongoClient.connect(
url
)
.then(client => {
console.log('Connected!');
_db = client.db();
callback();
})
.catch(err => {
console.log("DB NOT CONNECTED");
console.log(err);
});
};
const getDb = () => {
if (_db) {
return _db;
}
throw 'No db found';
}
exports.mongoConnect = mongoConnect;
exports.db = getDb;
models/product.js
const fs = require('fs');
const path = require('path');
const mongodb = require('mongodb');
const getDb = require('../util/db').db;
class Product {
constructor(title, description, price, id, userId) {
this.title = title;
this.description = description;
this.price = price;
this._id = id ? new mongodb.ObjectId(id) : null;
this.userId = userId;
}
save() {
const db = getDb();
db.collection('products')
.insertOne(this)
.then(res => {
console.log(res);
})
.catch(err => {
console.log(err);
});
}
//static means that it will be not called on concrete instance of product model, but will return all products
static fetchAll() {
const db = getDb();
return db
.collection('products')
.find()
.toArray()
.then(products => {
console.log(products);
return products;
})
.catch(err => {
console.log(err);
});
}
static fetchById(prodId) {
const db = getDb();
return db
.collection('products')
.find({ _id: new mongodb.ObjectId(prodId) })
.next()
.then(product => {
return product;
})
.catch(err => {
console.log(err);
})
}
static updateById(prodId, values) {
const db = getDb();
db
.collection('products')
.updateOne(
{ _id: new mongodb.ObjectId(prodId) },
{ $set: values }
);
}
}
module.exports = Product;
models/user.js
const mongodb = require('mongodb');
const getDb = require('../util/db').db;
const ObjectId = mongodb.ObjectId;
class User {
constructor(username, email, cart, id) {
this.name = username;
this.email = email;
this.cart = cart;
this._id = id;
}
save() {
const db = getDb();
db.collection('users').insertOne(this);
}
addToCart(product) {
//is product already in cart ?
const cartProductIndex = this.cart.items.findIndex(cp => {
return cp.productId.toString() == product._id.toString();
});
let newQuantity = 1;
const updatedCartItems = [...this.cart.items];
//if it is greater > -1, product is already there
if (cartProductIndex >= 0) {
newQuantity = this.cart.items[cartProductIndex].quantity + 1;
updatedCartItems[cartProductIndex].quantity = newQuantity;
} else {
updatedCartItems.push({
productId: new ObjectId(product._id),
quantity: newQuantity})
}
const updatedCart = {
items: updatedCartItems
};
const db = getDb();
return db.collection('users').updateOne(
{_id: new ObjectId(this._id)},
{ $set: { cart: updatedCart }}
);
}
getCart() {
const db = getDb();
const productIds = this.cart.items.map(i => {
return i.productId;
});
return db
.collection('products')
.find({ _id: { $in: productIds } })
.toArray()
.then(products => {
return products.map(p => {
return {
...p,
quantity: this.cart.items.find(i => {
return i.productId.toString() === p._id.toString();
}).quantity
};
});
});
}
static findById(userId) {
const db = getDb();
return db
.collection('users')
.findOne({ _id: new ObjectId(userId) })
.then(user => {
console.log(user);
return user;
})
.catch(err => {
console.log(err);
});
}
deleteItemFromCart(productId) {
const updatedCartItems = this.cart.items.filter(item => {
return item.productId.toString() !== productId.toString();
})
const db = getDb();
return db
.collection('users')
.updateOne(
{ _id: new ObjectId(this._id)},
{ $set: { cart: { items: updatedCartItems} } }
);
}
addOrder() {
const db = getDb();
return this.getCart().then(products => {
const order = {
items: products,
user: {
_id: new ObjectId(this._id),
name: this.name
}
};
return db.collection('orders')
.insertOne(order);
})
.then(result => {
this.cart = { items: [] };
return db
.collection('users')
.updateOne(
{ _id: new ObjectId(this._id) },
{ $set: { cart: { items:[] } } }
);
});
}
getOrders() {
const db = getDb();
return db.collection('orders')
.find({'user._id': new ObjectId(this._id)})
.toArray();
}
}
module.exports = User;
controllers/shop.js
const Product = require('../models/product');
exports.getAddProduct = (req, res, next) => {
res.render('product-add', {
pageTitle: 'Add product',
path: '/'
});
}
exports.postAddProduct = (req, res, next) => {
const product = new Product(
req.body.title,
req.body.description,
req.body.price,
null,
req.user._id);
product.save();
res.redirect('/');
}
exports.getProducts = (req, res, next) => {
Product.fetchAll().
then(products => {
res.render('shop', {
products: products,
pageTitle: 'shop',
path: '/',
hasProducts: products.length > 0,
activeShop: true
});
});
}
exports.getProduct = (req, res, next) => {
Product.fetchById(req.params.productId).
then(product => {
res.render('product', {
product: product,
pageTitle: 'product'
});
});
}
exports.getEditProduct = (req, res, next) => {
Product.fetchById(req.params.productId).
then(product => {
res.render('product-edit', {
product: product,
pageTitle: 'product'
});
});
}
exports.postUpdateProduct = (req, res, next) => {
Product.updateById(req.params.productId, req.body);
res.redirect('/');
}
exports.postCart = (req, res, next) => {
const prodId = req.body.productId;
Product.fetchById(prodId)
.then(product => {
return req.user.addToCart(product);
})
.then(result => {
console.log(result);
res.redirect('/cart');
})
}
exports.getCart = (req, res, next) => {
req.user
.getCart()
.then(products => {
res.render('shop/cart',{
path: '/cart',
pageTitle: 'Your cart',
products: products
})
});
}
exports.postCartDeleteProduct = (req, res, next) => {
const prodId = req.body.productId;
req.user
.deleteItemFromCart(prodId)
.then(result => {
res.redirect('/cart');
})
.catch(error => {
console.log(error);
})
}
exports.postOrder = (req, res, next) => {
let fetchedCart;
req.user
.addOrder()
.then(result => {
res.redirect('/orders');
})
.catch(error => {
console.log(error);
})
}
exports.getOrders = (req, res, next) => {
req.user
.getOrders()
.then(orders => {
res.render('shop/orders', {
path: '/orders',
pageTitle: 'Your Orders',
orders: orders
});
})
.catch(error => console.log(error));
}
routes/admin.js
const path = require('path');
const express = require('express');
const router = express.Router();
const productsController = require('../controllers/shop');
const products = [];
router.get('/product-add', productsController.getAddProduct);
router.post('/product-add', productsController.postAddProduct);
router.get('/product-edit/:productId', productsController.getEditProduct);
router.post('/product-update/:productId', productsController.postUpdateProduct);
module.exports = router;
routes/shop.js
const path = require('path');
const express = require('express');
const router = express.Router();
const shopController = require('../controllers/shop');
router.get('/', shopController.getProducts);
router.get('/products/:productId', shopController.getProduct);
router.post('/cart', shopController.postCart);
router.get('/cart', shopController.getCart);
router.post('/cart-delete-item', shopController.postCartDeleteProduct);
router.post('/create-order', shopController.postOrder);
router.get('/orders', shopController.getOrders);
module.exports = router;
views/includes/head.ejs
<!DOCTYPE html>
<html>
<head>
<!-- we are using express.static in app.js to serve static css file from public dir -->
<link rel="stylesheet" href="/css/main.css">
views/includes/navigation.ejs
<nav>
<ul>
<li>
<a href="/">
Products
</a>
</li>
<li>
<a href="/admin/product-add">
Add product
</a>
</li>
<li>
<a href="/cart">
Cart
</a>
</li>
<li>
<a href="/orders">
Orders
</a>
</li>
</ul>
</nav>
views/product-add.ejs
<%- include('includes/head.ejs') %>
<title><%= pageTitle %></title>
</head>
<body>
<%- include('includes/nav.ejs') %>
<main>
<h2>Add product</h2>
<form method="post" action="/admin/product-add">
<div>
<label for="title">
Title
</label>
<input type="text" name="title" />
</div>
<div>
<label for="description">
Description
</label>
<input type="text" name="description" />
</div>
<div>
<label for="price">
Price
</label>
<input type="text" name="price" />
</div>
<div>
<input type="submit" value="Submit" />
</div>
</form>
</main>
</body>
views/product-edit.ejs
<%- include('includes/head.ejs') %>
<title><%= pageTitle %></title>
</head>
<body>
<%- include('includes/nav.ejs') %>
<main>
<h2><%= product.title %></h2>
<div class="products">
<div>
<form method="post" action="/admin/product-update/<%= product._id %>">
<div>
<input type="text" name="description" value="<%= product.description %>" />
</div>
<div>
<input type="text" name="price" value="<%= product.price %>" />
</div>
<div>
<input type="submit" value="save" />
</div>
</form>
</div>
</div>
</main>
</body>
</html>
views/404.ejs
<%- include('includes/head.ejs') %>
</head>
<body>
<%- include('includes/navigation.ejs') %>
<h1>Page not found</h1>
</body>
</html>
views/shop.ejs
<%- include('includes/head.ejs') %>
<title><%= pageTitle %></title>
</head>
<body>
<%- include('includes/nav.ejs') %>
<main>
<h2>Products</h2>
<% if (products.length > 0) { %>
<div class="products">
<% products.forEach(function(item) { %>
<div>
<h3>
<%= item.title %>
</h3>
<div>
<%= item.description %>
</div>
<div>
<%= item.price %>
</div>
<div class="actions">
<a href="/products/<%= item._id %>">Detail</a>
<a href="/admin/product-edit/<%= item._id %>">Edit</a>
<form action="/cart" method="post">
<button class="btn" type="submit">Add to cart</button>
<input type="hidden" name="productId" value="<%= item._id %>" />
</form>
</div>
</div>
<% }); %>
</div>
<% } %>
</main>
</body>
</html>
views/product.ejs
<%- include('includes/head.ejs') %>
<title><%= pageTitle %></title>
</head>
<body>
<%- include('includes/nav.ejs') %>
<main>
<h2><%= product.title %></h2>
<div class="products">
<div>
<div>
<%= product.description %>
</div>
<div>
<%= product.price %>
</div>
</div>
</div>
</main>
</body>
</html>
views/shop/cart.ejs
<%- include('../includes/head.ejs') %>
<title><%= pageTitle %></title>
</head>
<body>
<%- include('../includes/nav.ejs') %>
<main>
<h2>Products</h2>
<% if (products.length > 0) { %>
<div class="products">
<% products.forEach(function(item) { %>
<div>
<h3>
<%= item.title %>
</h3>
<div>
Quantity: <%= item.quantity %>
</div>
<div class="actions">
<form action="/cart-delete-item" method="post">
<button class="btn" type="submit">Delete</button>
<input type="hidden" name="productId" value="<%= item._id %>" />
</form>
</div>
</div>
<% }); %>
</div>
<div class="actions">
<form action="/create-order" method="post">
<button class="btn" type="submit">Create Order</button>
</form>
</div>
<% } %>
</main>
</body>
</html>
views/shop/orders.ejs
<%- include('../includes/head.ejs') %>
<title><%= pageTitle %></title>
</head>
<body>
<%- include('../includes/nav.ejs') %>
<main>
<h2>Orders</h2>
<% if (orders.length > 0) { %>
<div class="orders">
<% orders.forEach(function(order) { %>
<div>
<h3>
Order <%= order._id %>
</h3>
<% order.items.forEach(function(item) { %>
<div>
<%= item.title %>
Quantity: <%= item.quantity %>
</div>
<% }); %>
</div>
<% }); %>
</div>
<% } %>
</main>
</body>
</html>
app.js
const path = require('path');
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
const shopRoutes = require('./routes/shop');
const adminRoutes = require('./routes/admin');
const mongoConnect = require('./util/db').mongoConnect;
const User = require('./models/user');
//set template engine to EJS
app.set('view engine', 'ejs');
app.set('views', 'views');
//add this to make public folder available to serve static files, like css
app.use(express.static(path.join(__dirname, 'public')));
app.use((req, res, next) => {
User.findById('5e3ed9cee844e413f1269e18')
.then(user => {
req.user = new User(user.name, user.email, user.cart, user._id);
next();
})
.catch(err => console.log(err));
})
app.use(bodyParser.urlencoded({exteneded: false}));
app.use(shopRoutes);
app.use('/admin', adminRoutes);
app.get('/test', (req, res, next) => {
res.send('<h1>testing</h1>');
});
app.get('/', (req, res, next) => {
res.send('<h1>hello</h1>');
});
mongoConnect(() => {
app.listen(3000);
});