
tsconfig.json
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
package.json
{
"name": "ecommerce-management",
"version": "1.0.0",
"main": "dist/app.js",
"scripts": {
"build": "tsc",
"start": "node dist/app.js",
"dev": "ts-node src/app.ts",
"seed": "ts-node src/seeds/seed.ts"
},
"dependencies": {
"bcrypt": "^5.1.1",
"dotenv": "^16.4.5",
"ejs": "^3.1.10",
"express": "^4.19.2",
"express-validator": "^7.2.0",
"jsonwebtoken": "^9.0.2",
"mongoose": "^8.6.1",
"mysql2": "^3.11.0",
"sequelize": "^6.37.3"
},
"devDependencies": {
"@types/bcrypt": "^5.0.2",
"@types/express": "^4.17.21",
"@types/jsonwebtoken": "^9.0.6",
"@types/node": "^22.4.1",
"ts-node": "^10.9.2",
"typescript": "^5.5.4"
}
}
.env
PORT=3000
DB_TYPE=mongodb # or mongodb
JWT_SECRET=a03f821509a1449f474ec881d1672b2091df81564e00fe181bd5f0b1b75185ad40fbe32ae79c5885512c60b10716984e0a4b2e0cb145babeb864bc5f6eeb6dde
# For MySQL
# MYSQL_HOST=localhost
# MYSQL_USER=root
# MYSQL_PASSWORD=password
# MYSQL_DATABASE=ecommerce_db
# For MongoDB
MONGODB_URI=mongodb://localhost:27017/ecommerce_db
app.ts
import express from 'express';
import dotenv from 'dotenv';
import path from 'path';
import { connectDB, initializeModels } from './config/db';
import userRoutes from './routes/userRoutes';
import categoryRoutes from './routes/categoryRoutes';
import subcategoryRoutes from './routes/subcategoryRoutes';
import productRoutes from './routes/productRoutes';
import errorHandler from './middlewares/errorHandler';
dotenv.config();
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// View engine setup
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, '../src/views')); // Adjusted to point to src/views/
// Root route
app.get('/', (req, res) => {
res.redirect('/users/login-view');
});
// Connect to DB and initialize models
const startServer = async () => {
try {
await connectDB();
initializeModels();
// Routes
app.use('/users', userRoutes);
app.use('/categories', categoryRoutes);
app.use('/subcategories', subcategoryRoutes);
app.use('/products', productRoutes);
// Error handling
app.use(errorHandler);
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
} catch (err) {
console.error('Failed to start server:', err);
process.exit(1);
}
};
startServer();
Seeder
import dotenv from 'dotenv';
import mongoose from 'mongoose';
import bcrypt from 'bcrypt'; // Add this import
import { User, Category, Subcategory, Product } from '../config/db';
dotenv.config();
async function seed() {
await mongoose.connect(process.env.MONGODB_URI!);
// Clear data
await User.deleteMany({});
await Category.deleteMany({});
await Subcategory.deleteMany({});
await Product.deleteMany({});
// Sample data
const user = await User.create({
username: 'admin',
email: 'admin@example.com',
password: await bcrypt.hash('password', 10)
});
const cat1 = await Category.create({ name: 'Electronics' });
const cat2 = await Category.create({ name: 'Books' });
const sub1 = await Subcategory.create({ name: 'Phones', categoryId: cat1._id });
const sub2 = await Subcategory.create({ name: 'Fiction', categoryId: cat2._id });
await Product.create({ name: 'iPhone', price: 999, categoryId: cat1._id, subcategoryId: sub1._id });
await Product.create({ name: 'Novel', price: 20, categoryId: cat2._id, subcategoryId: sub2._id });
console.log('Seeded MongoDB');
process.exit(0);
}
if (process.env.DB_TYPE === 'mongodb') {
seed();
} else {
console.log('Seed only for MongoDB');
}
Routes
import express from 'express';
import { validateRegister, validateLogin, register, login, getUsers } from '../controllers/userController';
import { authMiddleware } from '../middlewares/auth'; // Add this import
import { User } from '../config/db';
const router = express.Router();
// API routes
router.post('/register', validateRegister, register);
router.post('/login', validateLogin, login);
router.get('/', authMiddleware, getUsers);
// EJS views
router.get('/list', authMiddleware, async (req, res) => {
const users = process.env.DB_TYPE === 'mysql' ? await User.findAll() : await User.find(); // List view
res.render('users/list', { users });
});
router.get('/add', (req, res) => res.render('users/add')); // Register form
router.post('/add', validateRegister, register); // Handle form
router.get('/login-view', (req, res) => res.render('users/login'));
router.post('/login-view', validateLogin, login);
export default router;
import mongoose, { Schema } from 'mongoose';
const productSchema = new Schema({
name: { type: String, required: true },
price: { type: Number, required: true },
categoryId: { type: Schema.Types.ObjectId, ref: 'Category', required: true },
subcategoryId: { type: Schema.Types.ObjectId, ref: 'Subcategory', required: true },
});
export default mongoose.model('Product', productSchema);
Model Exmaple
import mongoose, { Schema } from 'mongoose';
const userSchema = new Schema({
username: { type: String, unique: true, required: true },
email: { type: String, unique: true, required: true },
password: { type: String, required: true },
});
export default mongoose.model('User', userSchema);
auth
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
export const authMiddleware = (req: Request, res: Response, next: NextFunction) => {
const token = req.header('Authorization')?.replace('Bearer ', '');
if (!token) return res.status(401).json({ message: 'No token' });
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET!);
(req as any).user = decoded;
next();
} catch (err) {
res.status(401).json({ message: 'Invalid token' });
}
};
errorHandler
import { Request, Response, NextFunction } from 'express';
export default (err: Error, req: Request, res: Response, next: NextFunction) => {
console.error(err.stack);
res.status(500).json({ message: 'Internal Server Error' });
};
Controller
import { Request, Response, NextFunction } from 'express';
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import { body, validationResult } from 'express-validator';
import { User } from '../config/db';
// Validation chains
export const validateRegister = [
body('username').notEmpty().trim(),
body('email').isEmail(),
body('password').isLength({ min: 6 }),
];
export const validateLogin = [
body('email').isEmail(),
body('password').notEmpty(),
];
// Register
export const register = async (req: Request, res: Response, next: NextFunction) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
try {
const { username, email, password } = req.body;
const hashedPassword = await bcrypt.hash(password, 10);
const user = await User.create({ username, email, password: hashedPassword });
res.status(201).json(user);
} catch (err) {
next(err);
}
};
// Login
export const login = async (req: Request, res: Response, next: NextFunction) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
try {
const { email, password } = req.body;
let user;
if (process.env.DB_TYPE === 'mysql') {
user = await User.findOne({ where: { email } });
} else {
user = await User.findOne({ email });
}
if (!user) return res.status(401).json({ message: 'Invalid credentials' });
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) return res.status(401).json({ message: 'Invalid credentials' });
const token = jwt.sign({ id: user.id || user._id }, process.env.JWT_SECRET!, { expiresIn: '1h' });
res.json({ token });
} catch (err) {
next(err);
}
};
// CRUD for users (protected)
export const getUsers = async (req: Request, res: Response, next: NextFunction) => {
try {
const users = process.env.DB_TYPE === 'mysql' ? await User.findAll() : await User.find();
res.json(users);
} catch (err) {
next(err);
}
};
export const getUser = async (req: Request, res: Response, next: NextFunction) => {
try {
const user = process.env.DB_TYPE === 'mysql' ? await User.findByPk(req.params.id) : await User.findById(req.params.id);
if (!user) return res.status(404).json({ message: 'User not found' });
res.json(user);
} catch (err) {
next(err);
}
};
import { Request, Response, NextFunction } from 'express';
import { body, validationResult } from 'express-validator';
import { Product, Category, Subcategory } from '../config/db';
// Validation chain
export const validateProduct = [
body('name').notEmpty().trim().withMessage('Name is required'),
body('price').isFloat({ min: 0 }).withMessage('Price must be a positive number'),
body('categoryId').notEmpty().withMessage('Category ID is required'),
body('subcategoryId').notEmpty().withMessage('Subcategory ID is required'),
];
// Create
export const createProduct = async (req: Request, res: Response, next: NextFunction) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
try {
const category = process.env.DB_TYPE === 'mysql' ? await Category.findByPk(req.body.categoryId) : await Category.findById(req.body.categoryId);
if (!category) return res.status(404).json({ message: 'Category not found' });
const subcategory = process.env.DB_TYPE === 'mysql' ? await Subcategory.findByPk(req.body.subcategoryId) : await Subcategory.findById(req.body.subcategoryId);
if (!subcategory) return res.status(404).json({ message: 'Subcategory not found' });
const product = await Product.create(req.body);
res.status(201).json(product);
} catch (err) {
next(err);
}
};
// Read all
export const getProducts = async (req: Request, res: Response, next: NextFunction) => {
try {
const products = process.env.DB_TYPE === 'mysql' ? await Product.findAll({ include: [Category, Subcategory] }) : await Product.find().populate(['categoryId', 'subcategoryId']);
res.json(products);
} catch (err) {
next(err);
}
};
// Read one
export const getProduct = async (req: Request, res: Response, next: NextFunction) => {
try {
const product = process.env.DB_TYPE === 'mysql' ? await Product.findByPk(req.params.id, { include: [Category, Subcategory] }) : await Product.findById(req.params.id).populate(['categoryId', 'subcategoryId']);
if (!product) return res.status(404).json({ message: 'Product not found' });
res.json(product);
} catch (err) {
next(err);
}
};
// Update
export const updateProduct = async (req: Request, res: Response, next: NextFunction) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
try {
const product = process.env.DB_TYPE === 'mysql' ? await Product.findByPk(req.params.id) : await Product.findById(req.params.id);
if (!product) return res.status(404).json({ message: 'Product not found' });
const category = process.env.DB_TYPE === 'mysql' ? await Category.findByPk(req.body.categoryId) : await Category.findById(req.body.categoryId);
if (!category) return res.status(404).json({ message: 'Category not found' });
const subcategory = process.env.DB_TYPE === 'mysql' ? await Subcategory.findByPk(req.body.subcategoryId) : await Subcategory.findById(req.body.subcategoryId);
if (!subcategory) return res.status(404).json({ message: 'Subcategory not found' });
await product.update(req.body);
res.json(product);
} catch (err) {
next(err);
}
};
// Delete
export const deleteProduct = async (req: Request, res: Response, next: NextFunction) => {
try {
const product = process.env.DB_TYPE === 'mysql' ? await Product.findByPk(req.params.id) : await Product.findById(req.params.id);
if (!product) return res.status(404).json({ message: 'Product not found' });
await product.destroy();
res.json({ message: 'Product deleted' });
} catch (err) {
next(err);
}
};
db
import dotenv from 'dotenv';
import { Sequelize } from 'sequelize';
import mongoose from 'mongoose';
dotenv.config();
// Initialize sequelize as non-null for MySQL
export let sequelize: Sequelize; // Non-null type
let db: typeof mongoose | null = null;
export const connectDB = async () => {
const DB_TYPE = process.env.DB_TYPE;
if (DB_TYPE === 'mysql') {
sequelize = new Sequelize(process.env.MYSQL_DATABASE!, process.env.MYSQL_USER!, process.env.MYSQL_PASSWORD, {
host: process.env.MYSQL_HOST,
dialect: 'mysql',
logging: false,
});
try {
await sequelize.authenticate();
console.log('MySQL connected');
await sequelize.sync({ alter: true });
} catch (error) {
console.error('MySQL connection error:', error);
process.exit(1);
}
} else if (DB_TYPE === 'mongodb') {
try {
db = await mongoose.connect(process.env.MONGODB_URI!);
console.log('MongoDB connected');
} catch (error) {
console.error('MongoDB connection error:', error);
process.exit(1);
}
} else {
throw new Error('Invalid DB_TYPE in .env');
}
};
// Export models based on DB_TYPE
export let User: any;
export let Category: any;
export let Subcategory: any;
export let Product: any;
// Load models after connectDB
export const initializeModels = () => {
if (process.env.DB_TYPE === 'mysql') {
User = require('../models/mysql/user').default;
Category = require('../models/mysql/category').default;
Subcategory = require('../models/mysql/subcategory').default;
Product = require('../models/mysql/product').default;
} else if (process.env.DB_TYPE === 'mongodb') {
User = require('../models/mongo/user').default;
Category = require('../models/mongo/category').default;
Subcategory = require('../models/mongo/subcategory').default;
Product = require('../models/mongo/product').default;
}
};