Node Js Structure

 

 

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;
  }
};