Module 1: Setting Up the Environment
In this module, we will focus on setting up the development environment for our Supply Chain Management (SCM) system. This involves installing necessary applications, setting up servers, and dependencies for the backend, as well as connecting to GitHub via Visual Studio Code. We will also create a schema/diagram of the application.
Step 1: Installing Node.js and npm
Node.js is a JavaScript runtime built on Chrome’s V8 JavaScript engine. npm (Node Package Manager) comes bundled with Node.js and allows us to install and manage dependencies for our project.
Download and Install Node.js:
- Visit the Node.js official website.
- Download the LTS version for your operating system.
- Follow the installation instructions.
Verify Installation:
node -v
npm -v
Step 2: Setting Up the Project
Create Project Directory:
mkdir scm-backend
cd scm-backend
Initialize npm:
npm init -y
Step 3: Installing Dependencies
Express.js: Express is a minimal and flexible Node.js web application framework.
npm install express
Other Essential Packages:
dotenv
for managing environment variables.mongoose
for interacting with MongoDB.jsonwebtoken
for JWT-based authentication.bcryptjs
for password hashing.cookie-parser
for parsing cookies.
npm install dotenv mongoose jsonwebtoken bcryptjs cookie-parser
Development Dependencies:
nodemon
for automatically restarting the server
npm install --save-dev nodemon
Step 4: Setting Up the Server
Create an entry point file server.js
and configure Express:
// server.js
const express = require('express');
const dotenv = require('dotenv');
// Load environment variables
dotenv.config();
const app = express();
const PORT = process.env.PORT || 5000;
// Middleware
app.use(express.json());
// Simple route
app.get('/', (req, res) => {
res.send('Welcome to SCM API');
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Add a start script in package.json
:
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
}
Step 5: Creating the Application Schema/Diagram
Before diving into code, it’s essential to have a clear architecture in mind. Here’s a high-level overview of our SCM API structure:
Entities and Relationships:
- Users: Handle authentication and roles (admin, employee, customer).
- Suppliers: Manage supplier information.
- Products: Track products and inventory.
- Orders: Manage customer orders.
- Employees: Handle employee details.
Application Flow:
- Authentication: JWT-based login and registration.
- CRUD Operations: Create, Read, Update, Delete for each entity.
- Advanced Filtering: Filter customers, orders, products, etc.
- Error Handling: Centralized error handling mechanism.
Step 6: Connecting to GitHub via Visual Studio Code
Initialize Git Repository:
git init
git add .
git commit -m "Initial commit"
Create GitHub Repository:
- Go to GitHub and create a new repository.
- Follow the instructions to push your local repository to GitHub.
Connect via Visual Studio Code:
- Open Visual Studio Code.
- Install the GitHub extension if not already installed.
- Clone the repository using the command palette (
Ctrl+Shift+P
).
Final Thoughts
In this first module, we’ve set up the development environment for our Supply Chain Management REST API. We’ve installed Node.js, Express, and other essential dependencies, created a basic server, and sketched out our application architecture. In the next module, we will design the database schema and define relationships between our entities.
Module 2: Database Design
In this module, we will design the database schema for our Supply Chain Management (SCM) system using MongoDB. MongoDB is a NoSQL database that stores data in a flexible, JSON-like format, making it suitable for handling complex data structures.
Entities and Attributes
We will define the following entities and their attributes:
Supplier
supplier_id
(Primary Key): Unique identifier for each supplier.name
: Name of the supplier.address
: Address of the supplier.contact_person
: Name of the contact person at the supplier.phone_number
: Phone number of the supplier.
Product
product_id
(Primary Key): Unique identifier for each product.name
: Name of the product.description
: Description of the product.unit_price
: Price per unit of the product.quantity_available
: Quantity of the product available in inventory.
Order
order_id
(Primary Key): Unique identifier for each order.product_id
(Foreign Key referencing Product): Identifier of the product ordered.supplier_id
(Foreign Key referencing Supplier): Identifier of the supplier from whom the product is ordered.order_date
: Date when the order was placed.quantity_ordered
: Quantity of the product ordered.
Shipment
shipment_id
(Primary Key): Unique identifier for each shipment.order_id
(Foreign Key referencing Order): Identifier of the order associated with the shipment.shipment_date
: Date when the shipment was sent.estimated_arrival_date
: Estimated arrival date of the shipment.actual_arrival_date
: Actual arrival date of the shipment.
Relationships Between Entities
Supplier to Product Relationship
- One-to-many relationship: Each supplier can supply multiple products.
- Foreign key:
supplier_id
in Product referencingsupplier_id
in Supplier.
Product to Order Relationship
- One-to-many relationship: Each product can be ordered multiple times.
- Foreign key:
product_id
in Order referencingproduct_id
in Product.
Order to Shipment Relationship
- One-to-one relationship: Each order can have one shipment.
- Foreign key:
order_id
in Shipment referencingorder_id
in Order.
MongoDB Schema Design
MongoDB schemas are defined using collections and documents. We will create the following collections: suppliers
, products
, orders
, and shipments
.
Supplier Collection
{
"supplier_id": "unique_identifier",
"name": "Supplier Name",
"address": "Supplier Address",
"contact_person": "Contact Person Name",
"phone_number": "Contact Phone Number"
}
Product Collection
{
"product_id": "unique_identifier",
"name": "Product Name",
"description": "Product Description",
"unit_price": 100.00,
"quantity_available": 50,
"supplier_id": "unique_identifier" // Foreign key referencing Supplier
}
Order Collection
{
"order_id": "unique_identifier",
"product_id": "unique_identifier", // Foreign key referencing Product
"supplier_id": "unique_identifier", // Foreign key referencing Supplier
"order_date": "2023-06-21T00:00:00Z",
"quantity_ordered": 10
}
Shipment Collection
{
"shipment_id": "unique_identifier",
"order_id": "unique_identifier", // Foreign key referencing Order
"shipment_date": "2023-06-22T00:00:00Z",
"estimated_arrival_date": "2023-06-25T00:00:00Z",
"actual_arrival_date": "2023-06-24T00:00:00Z"
}
Implementing the Database Schema in Mongoose
We will use Mongoose to define our schema models in a Node.js application.
Step 1: Install Mongoose
npm install mongoose
Step 2: Define the Models
Create a directory models
and define the models.
- Supplier Model
// models/Supplier.js
const mongoose = require('mongoose');
const SupplierSchema = new mongoose.Schema({
supplier_id: { type: String, required: true, unique: true },
name: { type: String, required: true },
address: { type: String, required: true },
contact_person: { type: String, required: true },
phone_number: { type: String, required: true }
});
module.exports = mongoose.model('Supplier', SupplierSchema);
Product Model
// models/Product.js
const mongoose = require('mongoose');
const ProductSchema = new mongoose.Schema({
product_id: { type: String, required: true, unique: true },
name: { type: String, required: true },
description: { type: String, required: true },
unit_price: { type: Number, required: true },
quantity_available: { type: Number, required: true },
supplier_id: { type: mongoose.Schema.Types.String, ref: 'Supplier', required: true }
});
module.exports = mongoose.model('Product', ProductSchema);
Order Model
// models/Order.js
const mongoose = require('mongoose');
const OrderSchema = new mongoose.Schema({
order_id: { type: String, required: true, unique: true },
product_id: { type: mongoose.Schema.Types.String, ref: 'Product', required: true },
supplier_id: { type: mongoose.Schema.Types.String, ref: 'Supplier', required: true },
order_date: { type: Date, required: true },
quantity_ordered: { type: Number, required: true }
});
module.exports = mongoose.model('Order', OrderSchema);
Shipment Model
// models/Shipment.js
const mongoose = require('mongoose');
const ShipmentSchema = new mongoose.Schema({
shipment_id: { type: String, required: true, unique: true },
order_id: { type: mongoose.Schema.Types.String, ref: 'Order', required: true },
shipment_date: { type: Date, required: true },
estimated_arrival_date: { type: Date, required: true },
actual_arrival_date: { type: Date, required: true }
});
module.exports = mongoose.model('Shipment', ShipmentSchema);
Connecting to MongoDB
In your server.js
file, add the connection to MongoDB using Mongoose:
// server.js
const express = require('express');
const mongoose = require('mongoose');
const dotenv = require('dotenv');
// Load environment variables
dotenv.config();
const app = express();
const PORT = process.env.PORT || 5000;
// Middleware
app.use(express.json());
// MongoDB connection
mongoose.connect(process.env.MONGO_URI, {
useNewUrlParser: true,
useUnifiedTopology: true
}).then(() => {
console.log('Connected to MongoDB');
}).catch((err) => {
console.error('Failed to connect to MongoDB', err);
});
// Simple route
app.get('/', (req, res) => {
res.send('Welcome to SCM API');
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
In this module, we have defined the database schema for our Supply Chain Management system using MongoDB. We created models for suppliers, products, orders, and shipments and established relationships between these entities. In the next module, we will implement user authentication and authorization with error validation, using JWT, bcrypt, and cookies.
Module 3: User Authentication and Authorization
In this module, we will implement user authentication and authorization for our Supply Chain Management (SCM) system. This includes user registration and login with error validation, full authorization, and authentication using JWT (JSON Web Tokens), bcrypt for password hashing, and cookies for session management. We will also add middleware and error handlers for enhanced security and better user experience.
Step 1: Setting Up Dependencies
- Install necessary packages:
npm install bcryptjs jsonwebtoken cookie-parser express-validator
Step 2: Define the User Model
Create a new model for the User in the models
directory.
// models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const UserSchema = new mongoose.Schema({
username: { type: String, required: true, unique: true },
email: { type: String, required: true, unique: true },
password: { type: String, required: true },
role: { type: String, enum: ['admin', 'employee', 'customer'], default: 'customer' }
});
// Pre-save middleware to hash password
UserSchema.pre('save', async function (next) {
if (!this.isModified('password')) return next();
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
next();
});
// Method to compare password
UserSchema.methods.matchPassword = async function (enteredPassword) {
return await bcrypt.compare(enteredPassword, this.password);
};
module.exports = mongoose.model('User', UserSchema);
Step 3: Create Authentication Middleware
Create a middleware to protect routes and ensure users are authenticated.
// middleware/auth.js
const jwt = require('jsonwebtoken');
const User = require('../models/User');
exports.protect = async (req, res, next) => {
let token;
if (req.cookies.token) {
token = req.cookies.token;
} else if (req.headers.authorization && req.headers.authorization.startsWith('Bearer')) {
token = req.headers.authorization.split(' ')[1];
}
if (!token) {
return res.status(401).json({ message: 'Not authorized to access this route' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = await User.findById(decoded.id);
next();
} catch (error) {
return res.status(401).json({ message: 'Not authorized to access this route' });
}
};
exports.authorize = (...roles) => {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({ message: `User role ${req.user.role} is not authorized to access this route` });
}
next();
};
};
Step 4: Create Auth Controller
Create a controller for handling user registration and login.
// controllers/authController.js
const User = require('../models/User');
const jwt = require('jsonwebtoken');
const { validationResult } = require('express-validator');
// Generate JWT
const generateToken = (id) => {
return jwt.sign({ id }, process.env.JWT_SECRET, { expiresIn: '1h' });
};
// Register User
exports.register = async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { username, email, password, role } = req.body;
try {
const user = await User.create({ username, email, password, role });
const token = generateToken(user._id);
res.cookie('token', token, { httpOnly: true });
res.status(201).json({ success: true, token });
} catch (error) {
res.status(400).json({ success: false, message: error.message });
}
};
// Login User
exports.login = async (req, res) => {
const { email, password } = req.body;
try {
const user = await User.findOne({ email }).select('+password');
if (!user || !(await user.matchPassword(password))) {
return res.status(401).json({ success: false, message: 'Invalid credentials' });
}
const token = generateToken(user._id);
res.cookie('token', token, { httpOnly: true });
res.status(200).json({ success: true, token });
} catch (error) {
res.status(400).json({ success: false, message: error.message });
}
};
// Logout User
exports.logout = (req, res) => {
res.cookie('token', 'none', { expires: new Date(Date.now() + 10 * 1000), httpOnly: true });
res.status(200).json({ success: true, message: 'User logged out successfully' });
};
Step 5: Create Auth Routes
Set up routes for user registration, login, and logout.
// routes/authRoutes.js
const express = require('express');
const { register, login, logout } = require('../controllers/authController');
const { check } = require('express-validator');
const router = express.Router();
router.post('/register', [
check('username', 'Username is required').not().isEmpty(),
check('email', 'Please include a valid email').isEmail(),
check('password', 'Password must be 6 or more characters').isLength({ min: 6 })
], register);
router.post('/login', [
check('email', 'Please include a valid email').isEmail(),
check('password', 'Password is required').exists()
], login);
router.get('/logout', logout);
module.exports = router;
Step 6: Integrate Routes and Middleware
Include the routes and middleware in your server configuration.
// server.js
const express = require('express');
const mongoose = require('mongoose');
const dotenv = require('dotenv');
const cookieParser = require('cookie-parser');
const authRoutes = require('./routes/authRoutes');
const { protect, authorize } = require('./middleware/auth');
// Load environment variables
dotenv.config();
const app = express();
const PORT = process.env.PORT || 5000;
// Middleware
app.use(express.json());
app.use(cookieParser());
// MongoDB connection
mongoose.connect(process.env.MONGO_URI, {
useNewUrlParser: true,
useUnifiedTopology: true
}).then(() => {
console.log('Connected to MongoDB');
}).catch((err) => {
console.error('Failed to connect to MongoDB', err);
});
// Routes
app.use('/api/auth', authRoutes);
// Protected route example
app.get('/api/private', protect, authorize('admin'), (req, res) => {
res.send('This is a private route for admins only');
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Step 7: Error Handling Middleware
Create a centralized error handling middleware.
// middleware/errorHandler.js
const errorHandler = (err, req, res, next) => {
const statusCode = err.statusCode || 500;
res.status(statusCode).json({
success: false,
message: err.message || 'Server Error'
});
};
module.exports = errorHandler;
Integrate the error handler in server.js
:
const errorHandler = require('./middleware/errorHandler');
// Other code...
// Error handler
app.use(errorHandler);
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
In this module, we have implemented user authentication and authorization for our Supply Chain Management system. This includes user registration, login, and logout with JWT, bcrypt, and cookies. We also added middleware for protecting routes and handling errors. In the next module, we will create the models for other entities in the SCM system.
Module 4: Models Creation
In this module, we will create the models for the other entities in our Supply Chain Management (SCM) system. These models will include Supplier
, Product
, Order
, and Shipment
, which we defined in Module 2. Each model will be defined using Mongoose, and we will also implement relationships between these models.
Step 1: Supplier Model
Create a file Supplier.js
in the models
directory.
// models/Supplier.js
const mongoose = require('mongoose');
const SupplierSchema = new mongoose.Schema({
supplier_id: { type: String, required: true, unique: true },
name: { type: String, required: true },
address: { type: String, required: true },
contact_person: { type: String, required: true },
phone_number: { type: String, required: true }
});
module.exports = mongoose.model('Supplier', SupplierSchema);
Step 2: Product Model
Create a file Product.js
in the models
directory.
// models/Product.js
const mongoose = require('mongoose');
const ProductSchema = new mongoose.Schema({
product_id: { type: String, required: true, unique: true },
name: { type: String, required: true },
description: { type: String, required: true },
unit_price: { type: Number, required: true },
quantity_available: { type: Number, required: true },
supplier_id: { type: mongoose.Schema.Types.String, ref: 'Supplier', required: true }
});
module.exports = mongoose.model('Product', ProductSchema);
Step 3: Order Model
Create a file Order.js
in the models
directory.
// models/Order.js
const mongoose = require('mongoose');
const OrderSchema = new mongoose.Schema({
order_id: { type: String, required: true, unique: true },
product_id: { type: mongoose.Schema.Types.String, ref: 'Product', required: true },
supplier_id: { type: mongoose.Schema.Types.String, ref: 'Supplier', required: true },
order_date: { type: Date, required: true },
quantity_ordered: { type: Number, required: true }
});
module.exports = mongoose.model('Order', OrderSchema);
Step 4: Shipment Model
Create a file Shipment.js
in the models
directory.
// models/Shipment.js
const mongoose = require('mongoose');
const ShipmentSchema = new mongoose.Schema({
shipment_id: { type: String, required: true, unique: true },
order_id: { type: mongoose.Schema.Types.String, ref: 'Order', required: true },
shipment_date: { type: Date, required: true },
estimated_arrival_date: { type: Date, required: true },
actual_arrival_date: { type: Date, required: true }
});
module.exports = mongoose.model('Shipment', ShipmentSchema);
Step 5: Creating Indexes and Relations
Indexes and relationships are crucial for efficient querying and maintaining data integrity. MongoDB (and Mongoose) supports these through schema definitions.
Adding Indexes
Indexes can be added to frequently queried fields to enhance performance.
For example, in the Product
model:
// models/Product.js
const mongoose = require('mongoose');
const ProductSchema = new mongoose.Schema({
product_id: { type: String, required: true, unique: true },
name: { type: String, required: true },
description: { type: String, required: true },
unit_price: { type: Number, required: true },
quantity_available: { type: Number, required: true },
supplier_id: { type: mongoose.Schema.Types.String, ref: 'Supplier', required: true }
});
// Adding indexes
ProductSchema.index({ name: 1 });
ProductSchema.index({ supplier_id: 1 });
module.exports = mongoose.model('Product', ProductSchema);
Defining Relations
Mongoose allows us to reference other documents within a collection, which helps in establishing relationships.
In the Order
model, we already referenced Product
and Supplier
:
// models/Order.js
const mongoose = require('mongoose');
const OrderSchema = new mongoose.Schema({
order_id: { type: String, required: true, unique: true },
product_id: { type: mongoose.Schema.Types.String, ref: 'Product', required: true },
supplier_id: { type: mongoose.Schema.Types.String, ref: 'Supplier', required: true },
order_date: { type: Date, required: true },
quantity_ordered: { type: Number, required: true }
});
// Populate method can be used to reference Product and Supplier data
OrderSchema.methods.populateReferences = function() {
return this.populate('product_id').populate('supplier_id');
};
module.exports = mongoose.model('Order', OrderSchema);
Step 6: Testing the Models
Before integrating these models into the broader application, it’s important to test their functionality.
Create a script testModels.js
to test the creation and relationships of these models:
// testModels.js
const mongoose = require('mongoose');
const dotenv = require('dotenv');
// Load environment variables
dotenv.config();
const Supplier = require('./models/Supplier');
const Product = require('./models/Product');
const Order = require('./models/Order');
const Shipment = require('./models/Shipment');
// MongoDB connection
mongoose.connect(process.env.MONGO_URI, {
useNewUrlParser: true,
useUnifiedTopology: true
}).then(() => {
console.log('Connected to MongoDB');
runTests();
}).catch((err) => {
console.error('Failed to connect to MongoDB', err);
});
async function runTests() {
try {
// Create a new supplier
const supplier = new Supplier({
supplier_id: 'SUP123',
name: 'Supplier A',
address: '123 Supplier St',
contact_person: 'John Doe',
phone_number: '1234567890'
});
await supplier.save();
// Create a new product
const product = new Product({
product_id: 'PROD123',
name: 'Product A',
description: 'Description of Product A',
unit_price: 10.00,
quantity_available: 100,
supplier_id: supplier.supplier_id
});
await product.save();
// Create a new order
const order = new Order({
order_id: 'ORD123',
product_id: product.product_id,
supplier_id: supplier.supplier_id,
order_date: new Date(),
quantity_ordered: 20
});
await order.save();
// Create a new shipment
const shipment = new Shipment({
shipment_id: 'SHIP123',
order_id: order.order_id,
shipment_date: new Date(),
estimated_arrival_date: new Date(new Date().setDate(new Date().getDate() + 5)),
actual_arrival_date: null
});
await shipment.save();
console.log('Test data created successfully');
} catch (error) {
console.error('Error creating test data', error);
} finally {
mongoose.connection.close();
}
}
Run the test script:
node testModels.js
In this module, we have created the models for the key entities in our Supply Chain Management system: Supplier
, Product
, Order
, and Shipment
. We also set up indexes and relationships between these entities. In the next module, we will create controllers and routes to handle CRUD operations for these models.
Module 5: Controllers and Routes Creation
In this module, we will create the controllers and routes to handle CRUD (Create, Read, Update, Delete) operations for the entities in our Supply Chain Management (SCM) system. We will define routes for suppliers, products, orders, and shipments, and implement the corresponding controller functions.
Step 1: Create Controllers
We will create a new directory controllers
and define the controller functions for each entity.
Supplier Controller
Create a file supplierController.js
in the controllers
directory.
// controllers/supplierController.js
const Supplier = require('../models/Supplier');
// Create a new supplier
exports.createSupplier = async (req, res) => {
try {
const supplier = new Supplier(req.body);
await supplier.save();
res.status(201).json(supplier);
} catch (error) {
res.status(400).json({ error: error.message });
}
};
// Get all suppliers
exports.getSuppliers = async (req, res) => {
try {
const suppliers = await Supplier.find();
res.status(200).json(suppliers);
} catch (error) {
res.status(400).json({ error: error.message });
}
};
// Get a single supplier by ID
exports.getSupplierById = async (req, res) => {
try {
const supplier = await Supplier.findById(req.params.id);
if (!supplier) {
return res.status(404).json({ message: 'Supplier not found' });
}
res.status(200).json(supplier);
} catch (error) {
res.status(400).json({ error: error.message });
}
};
// Update a supplier
exports.updateSupplier = async (req, res) => {
try {
const supplier = await Supplier.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true });
if (!supplier) {
return res.status(404).json({ message: 'Supplier not found' });
}
res.status(200).json(supplier);
} catch (error) {
res.status(400).json({ error: error.message });
}
};
// Delete a supplier
exports.deleteSupplier = async (req, res) => {
try {
const supplier = await Supplier.findByIdAndDelete(req.params.id);
if (!supplier) {
return res.status(404).json({ message: 'Supplier not found' });
}
res.status(200).json({ message: 'Supplier deleted successfully' });
} catch (error) {
res.status(400).json({ error: error.message });
}
};
Product Controller
Create a file productController.js
in the controllers
directory.
// controllers/productController.js
const Product = require('../models/Product');
// Create a new product
exports.createProduct = async (req, res) => {
try {
const product = new Product(req.body);
await product.save();
res.status(201).json(product);
} catch (error) {
res.status(400).json({ error: error.message });
}
};
// Get all products
exports.getProducts = async (req, res) => {
try {
const products = await Product.find();
res.status(200).json(products);
} catch (error) {
res.status(400).json({ error: error.message });
}
};
// Get a single product by ID
exports.getProductById = async (req, res) => {
try {
const product = await Product.findById(req.params.id);
if (!product) {
return res.status(404).json({ message: 'Product not found' });
}
res.status(200).json(product);
} catch (error) {
res.status(400).json({ error: error.message });
}
};
// Update a product
exports.updateProduct = async (req, res) => {
try {
const product = await Product.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true });
if (!product) {
return res.status(404).json({ message: 'Product not found' });
}
res.status(200).json(product);
} catch (error) {
res.status(400).json({ error: error.message });
}
};
// Delete a product
exports.deleteProduct = async (req, res) => {
try {
const product = await Product.findByIdAndDelete(req.params.id);
if (!product) {
return res.status(404).json({ message: 'Product not found' });
}
res.status(200).json({ message: 'Product deleted successfully' });
} catch (error) {
res.status(400).json({ error: error.message });
}
};
Order Controller
Create a file orderController.js
in the controllers
directory.
// controllers/orderController.js
const Order = require('../models/Order');
// Create a new order
exports.createOrder = async (req, res) => {
try {
const order = new Order(req.body);
await order.save();
res.status(201).json(order);
} catch (error) {
res.status(400).json({ error: error.message });
}
};
// Get all orders
exports.getOrders = async (req, res) => {
try {
const orders = await Order.find().populate('product_id supplier_id');
res.status(200).json(orders);
} catch (error) {
res.status(400).json({ error: error.message });
}
};
// Get a single order by ID
exports.getOrderById = async (req, res) => {
try {
const order = await Order.findById(req.params.id).populate('product_id supplier_id');
if (!order) {
return res.status(404).json({ message: 'Order not found' });
}
res.status(200).json(order);
} catch (error) {
res.status(400).json({ error: error.message });
}
};
// Update an order
exports.updateOrder = async (req, res) => {
try {
const order = await Order.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true });
if (!order) {
return res.status(404).json({ message: 'Order not found' });
}
res.status(200).json(order);
} catch (error) {
res.status(400).json({ error: error.message });
}
};
// Delete an order
exports.deleteOrder = async (req, res) => {
try {
const order = await Order.findByIdAndDelete(req.params.id);
if (!order) {
return res.status(404).json({ message: 'Order not found' });
}
res.status(200).json({ message: 'Order deleted successfully' });
} catch (error) {
res.status(400).json({ error: error.message });
}
};
Shipment Controller
Create a file shipmentController.js
in the controllers
directory.
// controllers/shipmentController.js
const Shipment = require('../models/Shipment');
// Create a new shipment
exports.createShipment = async (req, res) => {
try {
const shipment = new Shipment(req.body);
await shipment.save();
res.status(201).json(shipment);
} catch (error) {
res.status(400).json({ error: error.message });
}
};
// Get all shipments
exports.getShipments = async (req, res) => {
try {
const shipments = await Shipment.find().populate('order_id');
res.status(200).json(shipments);
} catch (error) {
res.status(400).json({ error: error.message });
}
};
// Get a single shipment by ID
exports.getShipmentById = async (req, res) => {
try {
const shipment = await Shipment.findById(req.params.id).populate('order_id');
if (!shipment) {
return res.status(404).json({ message: 'Shipment not found' });
}
res.status(200).json(shipment);
} catch (error) {
res.status(400).json({ error: error.message });
}
};
// Update a shipment
exports.updateShipment = async (req, res) => {
try {
const shipment = await Shipment.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true });
if (!shipment) {
return res.status(404).json({ message: 'Shipment not found' });
}
res.status(200).json(shipment);
} catch (error) {
res.status(400).json({ error: error.message });
}
};
// Delete a shipment
exports.deleteShipment = async (req, res) => {
try {
const shipment = await Shipment.findByIdAndDelete(req.params.id);
if (!shipment) {
return res.status(404).json({ message: 'Shipment not found' });
}
res.status(200).json({ message: 'Shipment deleted successfully' });
} catch (error) {
res.status(400).json({ error: error.message });
}
};
Step 2: Create Routes
We will create a new directory routes
and define the routes for each entity.
Supplier Routes
Create a file supplierRoutes.js
in the routes
directory.
// routes/supplierRoutes.js
const express = require('express');
const { createSupplier, getSuppliers, getSupplierById, updateSupplier, deleteSupplier } = require('../controllers/supplierController');
const { protect, authorize } = require('../middleware/auth');
const router = express.Router();
router.route('/')
.post(protect, authorize('admin'), createSupplier)
.get(protect, authorize('admin', 'employee'), getSuppliers);
router.route('/:id')
.get(protect, authorize('admin', 'employee'), getSupplierById)
.put(protect, authorize('admin'), updateSupplier)
.delete(protect, authorize('admin'), deleteSupplier);
module.exports = router;
Product Routes
Create a file productRoutes.js
in the routes
directory.
// routes/productRoutes.js
const express = require('express');
const { createProduct, getProducts, getProductById, updateProduct, deleteProduct } = require('../controllers/productController');
const { protect, authorize } = require('../middleware/auth');
const router = express.Router();
router.route('/')
.post(protect, authorize('admin'), createProduct)
.get(protect, authorize('admin', 'employee'), getProducts);
router.route('/:id')
.get(protect, authorize('admin', 'employee'), getProductById)
.put(protect, authorize('admin'), updateProduct)
.delete(protect, authorize('admin'), deleteProduct);
module.exports = router;
Order Routes
Create a file orderRoutes.js
in the routes
directory.
// routes/orderRoutes.js
const express = require('express');
const { createOrder, getOrders, getOrderById, updateOrder, deleteOrder } = require('../controllers/orderController');
const { protect, authorize } = require('../middleware/auth');
const router = express.Router();
router.route('/')
.post(protect, authorize('admin', 'employee'), createOrder)
.get(protect, authorize('admin', 'employee'), getOrders);
router.route('/:id')
.get(protect, authorize('admin', 'employee'), getOrderById)
.put(protect, authorize('admin', 'employee'), updateOrder)
.delete(protect, authorize('admin'), deleteOrder);
module.exports = router;
Shipment Routes
Create a file shipmentRoutes.js
in the routes
directory.
// routes/shipmentRoutes.js
const express = require('express');
const { createShipment, getShipments, getShipmentById, updateShipment, deleteShipment } = require('../controllers/shipmentController');
const { protect, authorize } = require('../middleware/auth');
const router = express.Router();
router.route('/')
.post(protect, authorize('admin', 'employee'), createShipment)
.get(protect, authorize('admin', 'employee'), getShipments);
router.route('/:id')
.get(protect, authorize('admin', 'employee'), getShipmentById)
.put(protect, authorize('admin', 'employee'), updateShipment)
.delete(protect, authorize('admin'), deleteShipment);
module.exports = router;
Step 3: Integrate Routes in the Main Server File
Include the newly created routes in your server.js
file.
// server.js
const express = require('express');
const mongoose = require('mongoose');
const dotenv = require('dotenv');
const cookieParser = require('cookie-parser');
const authRoutes = require('./routes/authRoutes');
const supplierRoutes = require('./routes/supplierRoutes');
const productRoutes = require('./routes/productRoutes');
const orderRoutes = require('./routes/orderRoutes');
const shipmentRoutes = require('./routes/shipmentRoutes');
const { protect, authorize } = require('./middleware/auth');
const errorHandler = require('./middleware/errorHandler');
// Load environment variables
dotenv.config();
const app = express();
const PORT = process.env.PORT || 5000;
// Middleware
app.use(express.json());
app.use(cookieParser());
// MongoDB connection
mongoose.connect(process.env.MONGO_URI, {
useNewUrlParser: true,
useUnifiedTopology: true
}).then(() => {
console.log('Connected to MongoDB');
}).catch((err) => {
console.error('Failed to connect to MongoDB', err);
});
// Routes
app.use('/api/auth', authRoutes);
app.use('/api/suppliers', supplierRoutes);
app.use('/api/products', productRoutes);
app.use('/api/orders', orderRoutes);
app.use('/api/shipments', shipmentRoutes);
// Error handler
app.use(errorHandler);
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
In this module, we have created controllers and routes for handling CRUD operations for suppliers, products, orders, and shipments in our Supply Chain Management system. Each route is protected and authorized based on user roles. In the next module, we will implement advanced pagination, filtering, and search functionalities.
Module 6: Advanced Pagination, Filtering, and Search
In this module, we will implement advanced pagination, filtering, and search functionalities directly within the controllers for our Supply Chain Management (SCM) system. This will enhance the usability of our API by allowing clients to fetch and query data more effectively.
Step 1: Implementing Pagination, Filtering, and Search
To achieve this, we will create reusable logic within each controller for handling pagination, filtering, and search. We will integrate this functionality into the Supplier
, Product
, Order
, and Shipment
controllers.
Step 2: Enhancing the Controllers
We will modify the existing controllers to include advanced querying capabilities.
Supplier Controller
Modify the supplierController.js
to include pagination, filtering, and search.
// controllers/supplierController.js
const Supplier = require('../models/Supplier');
// Get all suppliers with pagination, filtering, and search
exports.getSuppliers = async (req, res) => {
try {
const { page = 1, limit = 10, sort, select, ...filters } = req.query;
// Pagination
const pageNumber = parseInt(page);
const limitNumber = parseInt(limit);
const skip = (pageNumber - 1) * limitNumber;
// Filtering
const queryStr = JSON.stringify(filters);
const queryFilters = JSON.parse(
queryStr.replace(/\b(gt|gte|lt|lte|in)\b/g, match => `$${match}`)
);
let query = Supplier.find(queryFilters);
// Select Fields
if (select) {
const fields = select.split(',').join(' ');
query = query.select(fields);
}
// Sort
if (sort) {
const sortBy = sort.split(',').join(' ');
query = query.sort(sortBy);
} else {
query = query.sort('-createdAt');
}
const total = await Supplier.countDocuments(queryFilters);
const suppliers = await query.skip(skip).limit(limitNumber);
res.status(200).json({
success: true,
count: suppliers.length,
total,
data: suppliers
});
} catch (error) {
res.status(400).json({ error: error.message });
}
};
// Get a single supplier by ID
exports.getSupplierById = async (req, res) => {
try {
const supplier = await Supplier.findById(req.params.id);
if (!supplier) {
return res.status(404).json({ message: 'Supplier not found' });
}
res.status(200).json(supplier);
} catch (error) {
res.status(400).json({ error: error.message });
}
};
// Create a new supplier
exports.createSupplier = async (req, res) => {
try {
const supplier = new Supplier(req.body);
await supplier.save();
res.status(201).json(supplier);
} catch (error) {
res.status(400).json({ error: error.message });
}
};
// Update a supplier
exports.updateSupplier = async (req, res) => {
try {
const supplier = await Supplier.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true });
if (!supplier) {
return res.status(404).json({ message: 'Supplier not found' });
}
res.status(200).json(supplier);
} catch (error) {
res.status(400).json({ error: error.message });
}
};
// Delete a supplier
exports.deleteSupplier = async (req, res) => {
try {
const supplier = await Supplier.findByIdAndDelete(req.params.id);
if (!supplier) {
return res.status(404).json({ message: 'Supplier not found' });
}
res.status(200).json({ message: 'Supplier deleted successfully' });
} catch (error) {
res.status(400).json({ error: error.message });
}
};
Product Controller
Modify the productController.js
to include pagination, filtering, and search.
// controllers/productController.js
const Product = require('../models/Product');
// Get all products with pagination, filtering, and search
exports.getProducts = async (req, res) => {
try {
const { page = 1, limit = 10, sort, select, ...filters } = req.query;
// Pagination
const pageNumber = parseInt(page);
const limitNumber = parseInt(limit);
const skip = (pageNumber - 1) * limitNumber;
// Filtering
const queryStr = JSON.stringify(filters);
const queryFilters = JSON.parse(
queryStr.replace(/\b(gt|gte|lt|lte|in)\b/g, match => `$${match}`)
);
let query = Product.find(queryFilters);
// Select Fields
if (select) {
const fields = select.split(',').join(' ');
query = query.select(fields);
}
// Sort
if (sort) {
const sortBy = sort.split(',').join(' ');
query = query.sort(sortBy);
} else {
query = query.sort('-createdAt');
}
const total = await Product.countDocuments(queryFilters);
const products = await query.skip(skip).limit(limitNumber);
res.status(200).json({
success: true,
count: products.length,
total,
data: products
});
} catch (error) {
res.status(400).json({ error: error.message });
}
};
// Get a single product by ID
exports.getProductById = async (req, res) => {
try {
const product = await Product.findById(req.params.id);
if (!product) {
return res.status(404).json({ message: 'Product not found' });
}
res.status(200).json(product);
} catch (error) {
res.status(400).json({ error: error.message });
}
};
// Create a new product
exports.createProduct = async (req, res) => {
try {
const product = new Product(req.body);
await product.save();
res.status(201).json(product);
} catch (error) {
res.status(400).json({ error: error.message });
}
};
// Update a product
exports.updateProduct = async (req, res) => {
try {
const product = await Product.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true });
if (!product) {
return res.status(404).json({ message: 'Product not found' });
}
res.status(200).json(product);
} catch (error) {
res.status(400).json({ error: error.message });
}
};
// Delete a product
exports.deleteProduct = async (req, res) => {
try {
const product = await Product.findByIdAndDelete(req.params.id);
if (!product) {
return res.status(404).json({ message: 'Product not found' });
}
res.status(200).json({ message: 'Product deleted successfully' });
} catch (error) {
res.status(400).json({ error: error.message });
}
};
Order Controller
Modify the orderController.js
to include pagination, filtering, and search.
// controllers/orderController.js
const Order = require('../models/Order');
// Get all orders with pagination, filtering, and search
exports.getOrders = async (req, res) => {
try {
const { page = 1, limit = 10, sort, select, ...filters } = req.query;
// Pagination
const pageNumber = parseInt(page);
const limitNumber = parseInt(limit);
const skip = (pageNumber - 1) * limitNumber;
// Filtering
const queryStr = JSON.stringify(filters);
const queryFilters = JSON.parse(
queryStr.replace(/\b(gt|gte|lt|lte|in)\b/g, match => `$${match}`)
);
let query = Order.find(queryFilters).populate('product_id supplier_id');
// Select Fields
if (select) {
const fields = select.split(',').join(' ');
query = query.select(fields);
}
// Sort
if (sort) {
const sortBy = sort.split(',').join(' ');
query = query.sort(sortBy);
} else {
query = query.sort('-createdAt');
}
const total = await Order.countDocuments(queryFilters);
const orders = await query.skip(skip).limit(limitNumber);
res.status(200).json({
success: true,
count: orders.length,
total,
data: orders
});
} catch (error) {
res.status(400).json({ error: error.message });
}
};
// Get a single order by ID
exports.getOrderById = async (req, res) => {
try {
const order = await Order.findById(req.params.id).populate('product_id supplier_id');
if (!order) {
return res.status(404).json({ message: 'Order not found' });
}
res.status(200).json(order);
} catch (error) {
res.status(400).json({ error: error.message });
}
};
// Create a new order
exports.createOrder = async (req, res) => {
try {
const order = new Order(req.body);
await order.save();
res.status(201).json(order);
} catch (error) {
res.status(400).json({ error: error.message });
}
};
// Update an order
exports.updateOrder = async (req, res) => {
try {
const order = await Order.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true });
if (!order) {
return res.status(404).json({ message: 'Order not found' });
}
res.status(200).json(order);
} catch (error) {
res.status(400).json({ error: error.message });
}
};
// Delete an order
exports.deleteOrder = async (req, res) => {
try {
const order = await Order.findByIdAndDelete(req.params.id);
if (!order) {
return res.status(404).json({ message: 'Order not found' });
}
res.status(200).json({ message: 'Order deleted successfully' });
} catch (error) {
res.status(400).json({ error: error.message });
}
};
Shipment Controller
Modify the shipmentController.js
to include pagination, filtering, and search.
// controllers/shipmentController.js
const Shipment = require('../models/Shipment');
// Get all shipments with pagination, filtering, and search
exports.getShipments = async (req, res) => {
try {
const { page = 1, limit = 10, sort, select, ...filters } = req.query;
// Pagination
const pageNumber = parseInt(page);
const limitNumber = parseInt(limit);
const skip = (pageNumber - 1) * limitNumber;
// Filtering
const queryStr = JSON.stringify(filters);
const queryFilters = JSON.parse(
queryStr.replace(/\b(gt|gte|lt|lte|in)\b/g, match => `$${match}`)
);
let query = Shipment.find(queryFilters).populate('order_id');
// Select Fields
if (select) {
const fields = select.split(',').join(' ');
query = query.select(fields);
}
// Sort
if (sort) {
const sortBy = sort.split(',').join(' ');
query = query.sort(sortBy);
} else {
query = query.sort('-createdAt');
}
const total = await Shipment.countDocuments(queryFilters);
const shipments = await query.skip(skip).limit(limitNumber);
res.status(200).json({
success: true,
count: shipments.length,
total,
data: shipments
});
} catch (error) {
res.status(400).json({ error: error.message });
}
};
// Get a single shipment by ID
exports.getShipmentById = async (req, res) => {
try {
const shipment = await Shipment.findById(req.params.id).populate('order_id');
if (!shipment) {
return res.status(404).json({ message: 'Shipment not found' });
}
res.status(200).json(shipment);
} catch (error) {
res.status(400).json({ error: error.message });
}
};
// Create a new shipment
exports.createShipment = async (req, res) => {
try {
const shipment = new Shipment(req.body);
await shipment.save();
res.status(201).json(shipment);
} catch (error) {
res.status(400).json({ error: error.message });
}
};
// Update a shipment
exports.updateShipment = async (req, res) => {
try {
const shipment = await Shipment.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true });
if (!shipment) {
return res.status(404).json({ message: 'Shipment not found' });
}
res.status(200).json(shipment);
} catch (error) {
res.status(400).json({ error: error.message });
}
};
// Delete a shipment
exports.deleteShipment = async (req, res) => {
try {
const shipment = await Shipment.findByIdAndDelete(req.params.id);
if (!shipment) {
return res.status(404).json({ message: 'Shipment not found' });
}
res.status(200).json({ message: 'Shipment deleted successfully' });
} catch (error) {
res.status(400).json({ error: error.message });
}
};
In this module, we have enhanced the controllers for our Supply Chain Management system to include advanced pagination, filtering, and search functionalities. These features will allow clients to query data more effectively and efficiently. In the next module, we will focus on testing and deploying the application.
Module 7: Testing and Deployment
In this module, we will focus on testing our Supply Chain Management (SCM) API to ensure its functionality and reliability. After testing, we will deploy the application online.
Step 1: Testing the Application
Testing is a crucial part of the development process. We will use the following tools to test our API:
- Jest: A JavaScript testing framework.
- Supertest: A library for testing HTTP servers.
- MongoDB Memory Server: An in-memory MongoDB server for testing purposes.
Install Testing Dependencies
npm install --save-dev jest supertest mongodb-memory-server
Configure Jest
Add the following configuration to package.json
:
"scripts": {
"test": "jest"
},
"jest": {
"testEnvironment": "node"
}
Create Test Files
Create a directory tests
and add test files for each entity.
Supplier Tests
Create a file supplier.test.js
in the tests
directory:
// tests/supplier.test.js
const request = require('supertest');
const mongoose = require('mongoose');
const { MongoMemoryServer } = require('mongodb-memory-server');
const app = require('../server');
const Supplier = require('../models/Supplier');
let mongoServer;
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
const uri = mongoServer.getUri();
await mongoose.connect(uri, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
});
afterAll(async () => {
await mongoose.disconnect();
await mongoServer.stop();
});
describe('Supplier API', () => {
it('should create a new supplier', async () => {
const res = await request(app)
.post('/api/suppliers')
.send({
supplier_id: 'SUP123',
name: 'Supplier A',
address: '123 Supplier St',
contact_person: 'John Doe',
phone_number: '1234567890',
});
expect(res.statusCode).toEqual(201);
expect(res.body).toHaveProperty('supplier_id', 'SUP123');
});
it('should fetch all suppliers', async () => {
const res = await request(app).get('/api/suppliers');
expect(res.statusCode).toEqual(200);
expect(res.body.data).toHaveLength(1);
});
});
Product Tests
Create a file product.test.js
in the tests
directory:
// tests/product.test.js
const request = require('supertest');
const mongoose = require('mongoose');
const { MongoMemoryServer } = require('mongodb-memory-server');
const app = require('../server');
const Product = require('../models/Product');
let mongoServer;
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
const uri = mongoServer.getUri();
await mongoose.connect(uri, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
});
afterAll(async () => {
await mongoose.disconnect();
await mongoServer.stop();
});
describe('Product API', () => {
it('should create a new product', async () => {
const res = await request(app)
.post('/api/products')
.send({
product_id: 'PROD123',
name: 'Product A',
description: 'Description of Product A',
unit_price: 10.0,
quantity_available: 100,
supplier_id: 'SUP123',
});
expect(res.statusCode).toEqual(201);
expect(res.body).toHaveProperty('product_id', 'PROD123');
});
it('should fetch all products', async () => {
const res = await request(app).get('/api/products');
expect(res.statusCode).toEqual(200);
expect(res.body.data).toHaveLength(1);
});
});
Order Tests
Create a file order.test.js
in the tests
directory:
// tests/order.test.js
const request = require('supertest');
const mongoose = require('mongoose');
const { MongoMemoryServer } = require('mongodb-memory-server');
const app = require('../server');
const Order = require('../models/Order');
let mongoServer;
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
const uri = mongoServer.getUri();
await mongoose.connect(uri, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
});
afterAll(async () => {
await mongoose.disconnect();
await mongoServer.stop();
});
describe('Order API', () => {
it('should create a new order', async () => {
const res = await request(app)
.post('/api/orders')
.send({
order_id: 'ORD123',
product_id: 'PROD123',
supplier_id: 'SUP123',
order_date: new Date(),
quantity_ordered: 10,
});
expect(res.statusCode).toEqual(201);
expect(res.body).toHaveProperty('order_id', 'ORD123');
});
it('should fetch all orders', async () => {
const res = await request(app).get('/api/orders');
expect(res.statusCode).toEqual(200);
expect(res.body.data).toHaveLength(1);
});
});
Shipment Tests
Create a file shipment.test.js
in the tests
directory:
// tests/shipment.test.js
const request = require('supertest');
const mongoose = require('mongoose');
const { MongoMemoryServer } = require('mongodb-memory-server');
const app = require('../server');
const Shipment = require('../models/Shipment');
let mongoServer;
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
const uri = mongoServer.getUri();
await mongoose.connect(uri, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
});
afterAll(async () => {
await mongoose.disconnect();
await mongoServer.stop();
});
describe('Shipment API', () => {
it('should create a new shipment', async () => {
const res = await request(app)
.post('/api/shipments')
.send({
shipment_id: 'SHIP123',
order_id: 'ORD123',
shipment_date: new Date(),
estimated_arrival_date: new Date(new Date().setDate(new Date().getDate() + 5)),
actual_arrival_date: null,
});
expect(res.statusCode).toEqual(201);
expect(res.body).toHaveProperty('shipment_id', 'SHIP123');
});
it('should fetch all shipments', async () => {
const res = await request(app).get('/api/shipments');
expect(res.statusCode).toEqual(200);
expect(res.body.data).toHaveLength(1);
});
});
Step 2: Running the Tests
Run the tests using the following command:
npm test
This command will execute all the test files and provide a report on their success or failure.
Step 3: Deploying the Application
We will deploy the application to a cloud platform. For this example, we will use Heroku.
Step-by-Step Deployment on Heroku
Install the Heroku CLI:
Follow the instructions on the Heroku Dev Center to install the Heroku CLI.
Login to Heroku:
heroku login
Create a Heroku App:
heroku create scm-api-app
Add MongoDB Add-on:
heroku addons:create mongolab:sandbox
Set Environment Variables:
Set the environment variables for JWT_SECRET and any other variables in Heroku using the Heroku CLI or the Heroku dashboard.
heroku config:set JWT_SECRET=your_jwt_secret
Deploy the Application:
Initialize a Git repository if you haven’t already, add the Heroku remote, and push the code to Heroku.
git init
git add .
git commit -m "Initial commit"
git remote add heroku https://git.heroku.com/scm-api-app.git
git push heroku master
Open the App:
heroku open
Your SCM API should now be live on Heroku. You can use the Heroku dashboard to manage your app, view logs, and scale your application as needed.
Welcome to DevTechTutor.com, your ultimate resource for mastering web development and technology! Whether you're a beginner eager to dive into coding or an experienced developer looking to sharpen your skills, DevTechTutor.com is here to guide you every step of the way. Our mission is to make learning web development accessible, engaging, and effective.