Memahami Struktur Project Express yang Rapi - Perwira Learning Center


1. Latar Belakang

    Hari ini kita masih belajar Express.js di Perwira Learning Center dan akan membahas Struktur Project Express yang Rapi. Ini ibarat merapikan toko serba ada:

  • Toko berantakan = Semua barang dicampur, susah cari, mudah error
  • Toko rapi = Setiap barang ada tempatnya, mudah dicari, efisien

Struktur project yang baik adalah fondasi untuk aplikasi yang scalable, maintainable, dan kolaboratif. Di dunia industri, struktur yang rapi bukan pilihan tapi keharusan!

2. Alat dan Bahan

a. Perangkat Lunak

  1. Express.js - Framework utama
  2. Node.js & npm - Runtime dan package manager
  3. VS Code - Code editor
  4. Postman - API testing
  5. Git - Version control

b. Perangkat Keras

  1. Laptop/PC standar

3. Pembahasan

3.1 Masalah Struktur Project Berantakan

Contoh struktur berantakan (BAD):

text
project/
├── server.js          ← 500 baris kode!
├── database.js        ← Semua model dicampur
├── helpers.js         ← 20 fungsi random
├── auth.js            ← Auth logic
└── ...               ← Chaos total!

Masalah:

  1. God Object - Satu file melakukan segalanya
  2. Tight Coupling - Semua kode saling tergantung
  3. Poor Maintainability - Susah cari bug, susah tambah fitur
  4. Hard to Test - Tidak bisa test bagian tertentu
  5. Team Collaboration - Conflict di git, kerja tidak parallel

3.2 Prinsip Struktur Project yang Baik

SOLID Principles untuk Project Structure:

  1. Single Responsibility - Satu file, satu tanggung jawab
  2. Open/Closed - Bisa extend, tidak perlu modify file lama
  3. Liskov Substitution - Komponen bisa diganti
  4. Interface Segregation - Interface kecil-kecil
  5. Dependency Inversion - Bergantung pada abstraksi, bukan konkrit

3.3 Praktik Lengkap: E-commerce API dengan Struktur Rapi

Mari buat project dari nol dengan struktur yang rapi:

bash
# 1. Buat folder project
mkdir ecommerce-api
cd ecommerce-api

# 2. Inisialisasi project
npm init -y

# 3. Install dependencies
npm install express dotenv cors helmet morgan
npm install -D nodemon

# 4. Buat struktur folder
mkdir -p src/{config,controllers,middleware,models,repositories,routes,services,utils,validators}

Struktur akhir project:

text
ecommerce-api/
├── src/
│   ├── app.js              # Express app configuration
│   ├── server.js           # Server entry point
│   ├── config/             # Configuration files
│   │   ├── database.js     # DB connection
│   │   ├── constants.js    # App constants
│   │   └── env.js          # Environment config
│   ├── controllers/        # Request handlers
│   │   ├── auth.controller.js
│   │   ├── product.controller.js
│   │   └── order.controller.js
│   ├── middleware/         # Custom middleware
│   │   ├── auth.middleware.js
│   │   ├── error.middleware.js
│   │   └── validation.middleware.js
│   ├── models/             # Data models/Schemas
│   │   ├── User.model.js
│   │   ├── Product.model.js
│   │   └── Order.model.js
│   ├── repositories/       # Data access layer
│   │   ├── user.repository.js
│   │   ├── product.repository.js
│   │   └── order.repository.js
│   ├── routes/             # Route definitions
│   │   ├── auth.routes.js
│   │   ├── product.routes.js
│   │   ├── order.routes.js
│   │   └── index.js        # Route aggregator
│   ├── services/           # Business logic
│   │   ├── auth.service.js
│   │   ├── product.service.js
│   │   └── order.service.js
│   ├── utils/              # Utility functions
│   │   ├── apiResponse.js
│   │   ├── asyncHandler.js
│   │   └── logger.js
│   └── validators/         # Validation schemas
│       ├── auth.validator.js
│       ├── product.validator.js
│       └── order.validator.js
├── tests/                  # Test files
│   ├── unit/
│   └── integration/
├── public/                 # Static files
├── .env                    # Environment variables
├── .env.example            # Environment template
├── .gitignore             # Git ignore rules
├── package.json           # Dependencies
└── README.md              # Documentation

3.4 File-by-File Implementation

1. package.json

json
{
  "name": "ecommerce-api",
  "version": "1.0.0",
  "description": "E-commerce API dengan struktur rapi",
  "main": "src/server.js",
  "scripts": {
    "start": "node src/server.js",
    "dev": "nodemon src/server.js",
    "test": "jest",
    "test:watch": "jest --watch",
    "lint": "eslint src/",
    "lint:fix": "eslint src/ --fix",
    "format": "prettier --write src/"
  },
  "keywords": ["express", "ecommerce", "api", "structure"],
  "author": "Perwira Learning Center",
  "license": "MIT",
  "dependencies": {
    "cors": "^2.8.5",
    "dotenv": "^16.3.1",
    "express": "^4.18.2",
    "helmet": "^7.0.0",
    "morgan": "^1.10.0"
  },
  "devDependencies": {
    "nodemon": "^3.0.1"
  },
  "engines": {
    "node": ">=18.0.0"
  }
}

2. .env

env
# Application
NODE_ENV=development
PORT=3007
APP_NAME=Ecommerce API
APP_VERSION=1.0.0

# Security
JWT_SECRET=your_super_secret_jwt_key_change_in_production
JWT_EXPIRE=7d

# Database (simulated for now)
DB_HOST=localhost
DB_PORT=5432
DB_NAME=ecommerce_db
DB_USER=postgres
DB_PASSWORD=secret

# Rate Limiting
RATE_LIMIT_WINDOW_MS=900000 # 15 minutes
RATE_LIMIT_MAX_REQUESTS=100

# Logging
LOG_LEVEL=info

3. .gitignore

gitignore
# Dependencies
node_modules/

# Environment
.env
.env.local

# Logs
logs
*.log
npm-debug.log*

# Runtime data
.DS_Store
Thumbs.db

# IDE
.vscode/
.idea/
*.swp
*.swo

# Test coverage
coverage/
.nyc_output

# OS
tmp/
temp/

4. src/config/env.js

javascript
require('dotenv').config();

module.exports = {
  // App
  NODE_ENV: process.env.NODE_ENV || 'development',
  PORT: parseInt(process.env.PORT) || 3000,
  APP_NAME: process.env.APP_NAME || 'Express API',
  APP_VERSION: process.env.APP_VERSION || '1.0.0',
  
  // Security
  JWT_SECRET: process.env.JWT_SECRET || 'default_secret_key',
  JWT_EXPIRE: process.env.JWT_EXPIRE || '7d',
  
  // Database
  DB_HOST: process.env.DB_HOST || 'localhost',
  DB_PORT: parseInt(process.env.DB_PORT) || 5432,
  DB_NAME: process.env.DB_NAME || 'app_db',
  DB_USER: process.env.DB_USER || 'postgres',
  DB_PASSWORD: process.env.DB_PASSWORD || '',
  
  // Rate Limiting
  RATE_LIMIT_WINDOW_MS: parseInt(process.env.RATE_LIMIT_WINDOW_MS) || 900000,
  RATE_LIMIT_MAX_REQUESTS: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS) || 100,
  
  // Logging
  LOG_LEVEL: process.env.LOG_LEVEL || 'info',
  
  // CORS
  CORS_ORIGIN: process.env.CORS_ORIGIN || '*',
  
  // API
  API_PREFIX: process.env.API_PREFIX || '/api/v1'
};

5. src/config/constants.js

javascript
module.exports = {
  // HTTP Status Codes
  HTTP_STATUS: {
    OK: 200,
    CREATED: 201,
    BAD_REQUEST: 400,
    UNAUTHORIZED: 401,
    FORBIDDEN: 403,
    NOT_FOUND: 404,
    CONFLICT: 409,
    INTERNAL_SERVER_ERROR: 500,
    SERVICE_UNAVAILABLE: 503
  },
  
  // User Roles
  USER_ROLES: {
    ADMIN: 'admin',
    CUSTOMER: 'customer',
    SELLER: 'seller'
  },
  
  // Product Categories
  PRODUCT_CATEGORIES: {
    ELECTRONICS: 'electronics',
    FASHION: 'fashion',
    HOME: 'home',
    SPORTS: 'sports',
    BOOKS: 'books',
    OTHER: 'other'
  },
  
  // Order Status
  ORDER_STATUS: {
    PENDING: 'pending',
    PROCESSING: 'processing',
    SHIPPED: 'shipped',
    DELIVERED: 'delivered',
    CANCELLED: 'cancelled'
  },
  
  // Pagination
  PAGINATION: {
    DEFAULT_PAGE: 1,
    DEFAULT_LIMIT: 10,
    MAX_LIMIT: 100
  },
  
  // Validation
  VALIDATION: {
    PASSWORD_MIN_LENGTH: 8,
    NAME_MAX_LENGTH: 100,
    EMAIL_MAX_LENGTH: 255,
    PRODUCT_TITLE_MAX_LENGTH: 200,
    PRODUCT_DESC_MAX_LENGTH: 1000
  }
};

6. src/utils/logger.js

javascript
const winston = require('winston');
const config = require('../config/env');

// Define log levels
const levels = {
  error: 0,
  warn: 1,
  info: 2,
  http: 3,
  debug: 4
};

// Define colors for each level
const colors = {
  error: 'red',
  warn: 'yellow',
  info: 'green',
  http: 'magenta',
  debug: 'blue'
};

winston.addColors(colors);

// Define format
const format = winston.format.combine(
  winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }),
  winston.format.colorize({ all: true }),
  winston.format.printf(
    (info) => `${info.timestamp} ${info.level}: ${info.message}`
  )
);

// Define transports (where logs go)
const transports = [
  // Console transport
  new winston.transports.Console(),
  
  // File transport for errors
  new winston.transports.File({
    filename: 'logs/error.log',
    level: 'error'
  }),
  
  // File transport for all logs
  new winston.transports.File({ 
    filename: 'logs/combined.log' 
  })
];

// Create logger instance
const logger = winston.createLogger({
  level: config.LOG_LEVEL,
  levels,
  format,
  transports
});

module.exports = logger;

7. src/utils/apiResponse.js

javascript
const constants = require('../config/constants');

/**
 * Standard API success response
 */
const successResponse = (data = null, message = 'Success', metadata = {}) => {
  return {
    success: true,
    message,
    data,
    metadata: {
      timestamp: new Date().toISOString(),
      ...metadata
    }
  };
};

/**
 * Standard API error response
 */
const errorResponse = (message = 'Error', errorCode = 'INTERNAL_ERROR', details = null) => {
  return {
    success: false,
    error: {
      code: errorCode,
      message,
      details,
      timestamp: new Date().toISOString()
    }
  };
};

/**
 * Standard API response with status code
 */
const apiResponse = (res, statusCode, data = null, message = '', metadata = {}) => {
  const response = successResponse(data, message, metadata);
  return res.status(statusCode).json(response);
};

/**
 * Standard API error response with status code
 */
const apiError = (res, statusCode, message = '', errorCode = '', details = null) => {
  const response = errorResponse(message, errorCode, details);
  return res.status(statusCode).json(response);
};

module.exports = {
  successResponse,
  errorResponse,
  apiResponse,
  apiError
};

8. src/utils/asyncHandler.js

javascript
/**
 * Async handler to wrap async route handlers and catch errors
 */
const asyncHandler = (fn) => {
  return (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next);
  };
};

/**
 * Async handler for Express router
 */
const asyncRouteHandler = (routeHandler) => {
  return async (req, res, next) => {
    try {
      await routeHandler(req, res, next);
    } catch (error) {
      next(error);
    }
  };
};

module.exports = {
  asyncHandler,
  asyncRouteHandler
};

9. src/models/User.model.js

javascript
// In real app, this would be a database model
// For now, we'll use an in-memory representation

class User {
  constructor(id, email, name, password, role = 'customer') {
    this.id = id;
    this.email = email;
    this.name = name;
    this.password = password; // In real app, this would be hashed
    this.role = role;
    this.createdAt = new Date();
    this.updatedAt = new Date();
    this.isActive = true;
  }

  toJSON() {
    return {
      id: this.id,
      email: this.email,
      name: this.name,
      role: this.role,
      createdAt: this.createdAt,
      updatedAt: this.updatedAt,
      isActive: this.isActive
    };
  }
}

// Sample users
const users = [
  new User(1, 'admin@example.com', 'Admin User', 'admin123', 'admin'),
  new User(2, 'customer@example.com', 'John Doe', 'customer123', 'customer'),
  new User(3, 'seller@example.com', 'Jane Smith', 'seller123', 'seller')
];

module.exports = {
  User,
  users
};

10. src/models/Product.model.js

javascript
class Product {
  constructor(id, title, description, price, stock, category, sellerId) {
    this.id = id;
    this.title = title;
    this.description = description;
    this.price = price;
    this.stock = stock;
    this.category = category;
    this.sellerId = sellerId;
    this.createdAt = new Date();
    this.updatedAt = new Date();
    this.isActive = true;
    this.images = [];
    this.rating = 0;
    this.reviewCount = 0;
  }

  toJSON() {
    return {
      id: this.id,
      title: this.title,
      description: this.description,
      price: this.price,
      stock: this.stock,
      category: this.category,
      sellerId: this.sellerId,
      createdAt: this.createdAt,
      updatedAt: this.updatedAt,
      isActive: this.isActive,
      images: this.images,
      rating: this.rating,
      reviewCount: this.reviewCount
    };
  }
}

// Sample products
const products = [
  new Product(1, 'Laptop Gaming', 'Laptop untuk gaming', 15000000, 10, 'electronics', 3),
  new Product(2, 'T-shirt Casual', 'Kaos casual cotton', 150000, 50, 'fashion', 3),
  new Product(3, 'Smartphone', 'Smartphone terbaru', 8000000, 25, 'electronics', 3),
  new Product(4, 'Buku Programming', 'Buku belajar JavaScript', 250000, 100, 'books', 3)
];

module.exports = {
  Product,
  products
};

11. src/repositories/user.repository.js

javascript
const { users } = require('../models/User.model');
const logger = require('../utils/logger');

class UserRepository {
  constructor() {
    this.users = users;
    this.nextId = users.length + 1;
  }

  /**
   * Find all users with pagination
   */
  async findAll({ page = 1, limit = 10, role = null }) {
    try {
      let filteredUsers = [...this.users];
      
      // Filter by role if provided
      if (role) {
        filteredUsers = filteredUsers.filter(user => user.role === role);
      }
      
      // Pagination
      const startIndex = (page - 1) * limit;
      const endIndex = page * limit;
      
      const paginatedUsers = filteredUsers.slice(startIndex, endIndex);
      
      return {
        data: paginatedUsers.map(user => user.toJSON()),
        pagination: {
          page: parseInt(page),
          limit: parseInt(limit),
          totalItems: filteredUsers.length,
          totalPages: Math.ceil(filteredUsers.length / limit),
          hasNextPage: endIndex < filteredUsers.length,
          hasPrevPage: startIndex > 0
        }
      };
    } catch (error) {
      logger.error('Error in UserRepository.findAll:', error);
      throw error;
    }
  }

  /**
   * Find user by ID
   */
  async findById(id) {
    try {
      const user = this.users.find(u => u.id === parseInt(id));
      return user ? user.toJSON() : null;
    } catch (error) {
      logger.error('Error in UserRepository.findById:', error);
      throw error;
    }
  }

  /**
   * Find user by email
   */
  async findByEmail(email) {
    try {
      const user = this.users.find(u => u.email === email);
      return user ? user.toJSON() : null;
    } catch (error) {
      logger.error('Error in UserRepository.findByEmail:', error);
      throw error;
    }
  }

  /**
   * Create new user
   */
  async create(userData) {
    try {
      const newUser = {
        id: this.nextId++,
        ...userData,
        createdAt: new Date(),
        updatedAt: new Date(),
        isActive: true
      };
      
      this.users.push(newUser);
      return newUser;
    } catch (error) {
      logger.error('Error in UserRepository.create:', error);
      throw error;
    }
  }

  /**
   * Update user
   */
  async update(id, updateData) {
    try {
      const index = this.users.findIndex(u => u.id === parseInt(id));
      
      if (index === -1) {
        return null;
      }
      
      this.users[index] = {
        ...this.users[index],
        ...updateData,
        updatedAt: new Date()
      };
      
      return this.users[index].toJSON();
    } catch (error) {
      logger.error('Error in UserRepository.update:', error);
      throw error;
    }
  }

  /**
   * Delete user (soft delete)
   */
  async delete(id) {
    try {
      const index = this.users.findIndex(u => u.id === parseInt(id));
      
      if (index === -1) {
        return false;
      }
      
      this.users[index].isActive = false;
      this.users[index].updatedAt = new Date();
      
      return true;
    } catch (error) {
      logger.error('Error in UserRepository.delete:', error);
      throw error;
    }
  }
}

module.exports = new UserRepository();

12. src/repositories/product.repository.js

javascript
const { products } = require('../models/Product.model');
const logger = require('../utils/logger');

class ProductRepository {
  constructor() {
    this.products = products;
    this.nextId = products.length + 1;
  }

  async findAll({ 
    page = 1, 
    limit = 10, 
    category = null, 
    minPrice = null, 
    maxPrice = null,
    search = null,
    sellerId = null
  }) {
    try {
      let filteredProducts = [...this.products].filter(p => p.isActive);
      
      // Apply filters
      if (category) {
        filteredProducts = filteredProducts.filter(p => p.category === category);
      }
      
      if (minPrice !== null) {
        filteredProducts = filteredProducts.filter(p => p.price >= minPrice);
      }
      
      if (maxPrice !== null) {
        filteredProducts = filteredProducts.filter(p => p.price <= maxPrice);
      }
      
      if (search) {
        const searchLower = search.toLowerCase();
        filteredProducts = filteredProducts.filter(p => 
          p.title.toLowerCase().includes(searchLower) ||
          p.description.toLowerCase().includes(searchLower)
        );
      }
      
      if (sellerId) {
        filteredProducts = filteredProducts.filter(p => p.sellerId === parseInt(sellerId));
      }
      
      // Pagination
      const startIndex = (page - 1) * limit;
      const endIndex = page * limit;
      
      const paginatedProducts = filteredProducts.slice(startIndex, endIndex);
      
      return {
        data: paginatedProducts.map(product => product.toJSON()),
        pagination: {
          page: parseInt(page),
          limit: parseInt(limit),
          totalItems: filteredProducts.length,
          totalPages: Math.ceil(filteredProducts.length / limit),
          hasNextPage: endIndex < filteredProducts.length,
          hasPrevPage: startIndex > 0
        },
        filters: {
          category,
          priceRange: { min: minPrice, max: maxPrice },
          search,
          sellerId
        }
      };
    } catch (error) {
      logger.error('Error in ProductRepository.findAll:', error);
      throw error;
    }
  }

  async findById(id) {
    try {
      const product = this.products.find(p => p.id === parseInt(id) && p.isActive);
      return product ? product.toJSON() : null;
    } catch (error) {
      logger.error('Error in ProductRepository.findById:', error);
      throw error;
    }
  }

  async create(productData) {
    try {
      const newProduct = {
        id: this.nextId++,
        ...productData,
        createdAt: new Date(),
        updatedAt: new Date(),
        isActive: true,
        images: productData.images || [],
        rating: 0,
        reviewCount: 0
      };
      
      this.products.push(newProduct);
      return newProduct.toJSON();
    } catch (error) {
      logger.error('Error in ProductRepository.create:', error);
      throw error;
    }
  }

  async update(id, updateData) {
    try {
      const index = this.products.findIndex(p => p.id === parseInt(id));
      
      if (index === -1) {
        return null;
      }
      
      this.products[index] = {
        ...this.products[index],
        ...updateData,
        updatedAt: new Date()
      };
      
      return this.products[index].toJSON();
    } catch (error) {
      logger.error('Error in ProductRepository.update:', error);
      throw error;
    }
  }

  async delete(id) {
    try {
      const index = this.products.findIndex(p => p.id === parseInt(id));
      
      if (index === -1) {
        return false;
      }
      
      this.products[index].isActive = false;
      this.products[index].updatedAt = new Date();
      
      return true;
    } catch (error) {
      logger.error('Error in ProductRepository.delete:', error);
      throw error;
    }
  }
}

module.exports = new ProductRepository();

13. src/services/auth.service.js

javascript
const userRepository = require('../repositories/user.repository');
const { apiError } = require('../utils/apiResponse');
const logger = require('../utils/logger');
const constants = require('../config/constants');

class AuthService {
  /**
   * Register new user
   */
  async register(userData) {
    try {
      logger.info('AuthService.register: Attempting to register user');
      
      // Check if user already exists
      const existingUser = await userRepository.findByEmail(userData.email);
      if (existingUser) {
        logger.warn(`AuthService.register: Email ${userData.email} already exists`);
        throw {
          statusCode: constants.HTTP_STATUS.CONFLICT,
          errorCode: 'EMAIL_ALREADY_EXISTS',
          message: 'Email already registered'
        };
      }
      
      // Validate password strength
      if (userData.password.length < constants.VALIDATION.PASSWORD_MIN_LENGTH) {
        throw {
          statusCode: constants.HTTP_STATUS.BAD_REQUEST,
          errorCode: 'WEAK_PASSWORD',
          message: `Password must be at least ${constants.VALIDATION.PASSWORD_MIN_LENGTH} characters`
        };
      }
      
      // Create user
      const newUser = await userRepository.create(userData);
      
      logger.info(`AuthService.register: User created with ID ${newUser.id}`);
      
      // Remove password from response
      const { password, ...userWithoutPassword } = newUser;
      
      return {
        user: userWithoutPassword,
        token: this.generateToken(newUser.id, newUser.role)
      };
    } catch (error) {
      logger.error('AuthService.register error:', error);
      throw error;
    }
  }

  /**
   * Login user
   */
  async login(email, password) {
    try {
      logger.info(`AuthService.login: Attempting login for ${email}`);
      
      // Find user by email
      const user = await userRepository.findByEmail(email);
      if (!user) {
        logger.warn(`AuthService.login: User not found for email ${email}`);
        throw {
          statusCode: constants.HTTP_STATUS.UNAUTHORIZED,
          errorCode: 'INVALID_CREDENTIALS',
          message: 'Invalid email or password'
        };
      }
      
      // Check if user is active
      if (!user.isActive) {
        logger.warn(`AuthService.login: User ${user.id} is inactive`);
        throw {
          statusCode: constants.HTTP_STATUS.FORBIDDEN,
          errorCode: 'ACCOUNT_INACTIVE',
          message: 'Account is inactive'
        };
      }
      
      // Check password (in real app, compare hashed passwords)
      if (user.password !== password) {
        logger.warn(`AuthService.login: Invalid password for user ${user.id}`);
        throw {
          statusCode: constants.HTTP_STATUS.UNAUTHORIZED,
          errorCode: 'INVALID_CREDENTIALS',
          message: 'Invalid email or password'
        };
      }
      
      // Remove password from response
      const { password: _, ...userWithoutPassword } = user;
      
      logger.info(`AuthService.login: User ${user.id} logged in successfully`);
      
      return {
        user: userWithoutPassword,
        token: this.generateToken(user.id, user.role)
      };
    } catch (error) {
      logger.error('AuthService.login error:', error);
      throw error;
    }
  }

  /**
   * Get user profile
   */
  async getProfile(userId) {
    try {
      logger.info(`AuthService.getProfile: Getting profile for user ${userId}`);
      
      const user = await userRepository.findById(userId);
      if (!user) {
        logger.warn(`AuthService.getProfile: User ${userId} not found`);
        throw {
          statusCode: constants.HTTP_STATUS.NOT_FOUND,
          errorCode: 'USER_NOT_FOUND',
          message: 'User not found'
        };
      }
      
      // Remove password
      const { password, ...userWithoutPassword } = user;
      
      return userWithoutPassword;
    } catch (error) {
      logger.error('AuthService.getProfile error:', error);
      throw error;
    }
  }

  /**
   * Update user profile
   */
  async updateProfile(userId, updateData) {
    try {
      logger.info(`AuthService.updateProfile: Updating profile for user ${userId}`);
      
      // Don't allow updating email via this endpoint
      if (updateData.email) {
        delete updateData.email;
      }
      
      const updatedUser = await userRepository.update(userId, updateData);
      if (!updatedUser) {
        logger.warn(`AuthService.updateProfile: User ${userId} not found`);
        throw {
          statusCode: constants.HTTP_STATUS.NOT_FOUND,
          errorCode: 'USER_NOT_FOUND',
          message: 'User not found'
        };
      }
      
      // Remove password
      const { password, ...userWithoutPassword } = updatedUser;
      
      logger.info(`AuthService.updateProfile: User ${userId} updated successfully`);
      
      return userWithoutPassword;
    } catch (error) {
      logger.error('AuthService.updateProfile error:', error);
      throw error;
    }
  }

  /**
   * Generate JWT token (simplified version)
   */
  generateToken(userId, role) {
    // In real app, use jsonwebtoken library
    const payload = {
      sub: userId,
      role: role,
      iat: Date.now()
    };
    
    // Simulate JWT token generation
    return `jwt_${Buffer.from(JSON.stringify(payload)).toString('base64')}`;
  }
}

module.exports = new AuthService();

14. src/services/product.service.js

javascript
const productRepository = require('../repositories/product.repository');
const logger = require('../utils/logger');
const constants = require('../config/constants');

class ProductService {
  /**
   * Get all products with filters
   */
  async getProducts(filters) {
    try {
      logger.info('ProductService.getProducts: Fetching products with filters', { filters });
      
      const result = await productRepository.findAll(filters);
      
      logger.info(`ProductService.getProducts: Found ${result.data.length} products`);
      
      return result;
    } catch (error) {
      logger.error('ProductService.getProducts error:', error);
      throw error;
    }
  }

  /**
   * Get product by ID
   */
  async getProductById(id) {
    try {
      logger.info(`ProductService.getProductById: Fetching product ${id}`);
      
      const product = await productRepository.findById(id);
      if (!product) {
        logger.warn(`ProductService.getProductById: Product ${id} not found`);
        throw {
          statusCode: constants.HTTP_STATUS.NOT_FOUND,
          errorCode: 'PRODUCT_NOT_FOUND',
          message: 'Product not found'
        };
      }
      
      logger.info(`ProductService.getProductById: Product ${id} found`);
      
      return product;
    } catch (error) {
      logger.error('ProductService.getProductById error:', error);
      throw error;
    }
  }

  /**
   * Create new product
   */
  async createProduct(productData, sellerId) {
    try {
      logger.info(`ProductService.createProduct: Creating product for seller ${sellerId}`);
      
      // Validate required fields
      const requiredFields = ['title', 'description', 'price', 'category'];
      const missingFields = requiredFields.filter(field => !productData[field]);
      
      if (missingFields.length > 0) {
        logger.warn(`ProductService.createProduct: Missing required fields: ${missingFields.join(', ')}`);
        throw {
          statusCode: constants.HTTP_STATUS.BAD_REQUEST,
          errorCode: 'MISSING_REQUIRED_FIELDS',
          message: `Missing required fields: ${missingFields.join(', ')}`,
          missingFields
        };
      }
      
      // Validate price
      if (productData.price <= 0) {
        logger.warn(`ProductService.createProduct: Invalid price ${productData.price}`);
        throw {
          statusCode: constants.HTTP_STATUS.BAD_REQUEST,
          errorCode: 'INVALID_PRICE',
          message: 'Price must be greater than 0'
        };
      }
      
      // Validate stock
      if (productData.stock !== undefined && productData.stock < 0) {
        logger.warn(`ProductService.createProduct: Invalid stock ${productData.stock}`);
        throw {
          statusCode: constants.HTTP_STATUS.BAD_REQUEST,
          errorCode: 'INVALID_STOCK',
          message: 'Stock cannot be negative'
        };
      }
      
      // Add seller ID
      const productWithSeller = {
        ...productData,
        sellerId: parseInt(sellerId),
        stock: productData.stock || 0
      };
      
      const newProduct = await productRepository.create(productWithSeller);
      
      logger.info(`ProductService.createProduct: Product created with ID ${newProduct.id}`);
      
      return newProduct;
    } catch (error) {
      logger.error('ProductService.createProduct error:', error);
      throw error;
    }
  }

  /**
   * Update product
   */
  async updateProduct(id, updateData, sellerId) {
    try {
      logger.info(`ProductService.updateProduct: Updating product ${id}`);
      
      // Check if product exists and belongs to seller
      const existingProduct = await productRepository.findById(id);
      if (!existingProduct) {
        logger.warn(`ProductService.updateProduct: Product ${id} not found`);
        throw {
          statusCode: constants.HTTP_STATUS.NOT_FOUND,
          errorCode: 'PRODUCT_NOT_FOUND',
          message: 'Product not found'
        };
      }
      
      // Check authorization (only seller can update their own product)
      if (existingProduct.sellerId !== parseInt(sellerId)) {
        logger.warn(`ProductService.updateProduct: Seller ${sellerId} not authorized to update product ${id}`);
        throw {
          statusCode: constants.HTTP_STATUS.FORBIDDEN,
          errorCode: 'UNAUTHORIZED',
          message: 'You can only update your own products'
        };
      }
      
      // Don't allow updating sellerId
      if (updateData.sellerId) {
        delete updateData.sellerId;
      }
      
      const updatedProduct = await productRepository.update(id, updateData);
      
      logger.info(`ProductService.updateProduct: Product ${id} updated successfully`);
      
      return updatedProduct;
    } catch (error) {
      logger.error('ProductService.updateProduct error:', error);
      throw error;
    }
  }

  /**
   * Delete product (soft delete)
   */
  async deleteProduct(id, sellerId) {
    try {
      logger.info(`ProductService.deleteProduct: Deleting product ${id}`);
      
      // Check if product exists and belongs to seller
      const existingProduct = await productRepository.findById(id);
      if (!existingProduct) {
        logger.warn(`ProductService.deleteProduct: Product ${id} not found`);
        throw {
          statusCode: constants.HTTP_STATUS.NOT_FOUND,
          errorCode: 'PRODUCT_NOT_FOUND',
          message: 'Product not found'
        };
      }
      
      // Check authorization
      if (existingProduct.sellerId !== parseInt(sellerId)) {
        logger.warn(`ProductService.deleteProduct: Seller ${sellerId} not authorized to delete product ${id}`);
        throw {
          statusCode: constants.HTTP_STATUS.FORBIDDEN,
          errorCode: 'UNAUTHORIZED',
          message: 'You can only delete your own products'
        };
      }
      
      const deleted = await productRepository.delete(id);
      
      if (!deleted) {
        throw {
          statusCode: constants.HTTP_STATUS.INTERNAL_SERVER_ERROR,
          errorCode: 'DELETE_FAILED',
          message: 'Failed to delete product'
        };
      }
      
      logger.info(`ProductService.deleteProduct: Product ${id} deleted successfully`);
      
      return { success: true, message: 'Product deleted successfully' };
    } catch (error) {
      logger.error('ProductService.deleteProduct error:', error);
      throw error;
    }
  }
}

module.exports = new ProductService();

15. src/validators/auth.validator.js

javascript
const Joi = require('joi');
const constants = require('../config/constants');

const authValidator = {
  // Register validation schema
  register: Joi.object({
    name: Joi.string()
      .min(2)
      .max(constants.VALIDATION.NAME_MAX_LENGTH)
      .required()
      .messages({
        'string.empty': 'Name is required',
        'string.min': 'Name must be at least 2 characters',
        'string.max': `Name must not exceed ${constants.VALIDATION.NAME_MAX_LENGTH} characters`
      }),
    
    email: Joi.string()
      .email()
      .max(constants.VALIDATION.EMAIL_MAX_LENGTH)
      .required()
      .messages({
        'string.empty': 'Email is required',
        'string.email': 'Please provide a valid email address',
        'string.max': `Email must not exceed ${constants.VALIDATION.EMAIL_MAX_LENGTH} characters`
      }),
    
    password: Joi.string()
      .min(constants.VALIDATION.PASSWORD_MIN_LENGTH)
      .required()
      .messages({
        'string.empty': 'Password is required',
        'string.min': `Password must be at least ${constants.VALIDATION.PASSWORD_MIN_LENGTH} characters`
      }),
    
    role: Joi.string()
      .valid(...Object.values(constants.USER_ROLES))
      .default(constants.USER_ROLES.CUSTOMER)
      .messages({
        'any.only': 'Invalid user role'
      })
  }),
  
  // Login validation schema
  login: Joi.object({
    email: Joi.string()
      .email()
      .required()
      .messages({
        'string.empty': 'Email is required',
        'string.email': 'Please provide a valid email address'
      }),
    
    password: Joi.string()
      .required()
      .messages({
        'string.empty': 'Password is required'
      })
  }),
  
  // Update profile validation schema
  updateProfile: Joi.object({
    name: Joi.string()
      .min(2)
      .max(constants.VALIDATION.NAME_MAX_LENGTH)
      .messages({
        'string.min': 'Name must be at least 2 characters',
        'string.max': `Name must not exceed ${constants.VALIDATION.NAME_MAX_LENGTH} characters`
      }),
    
    // Add other fields that can be updated
  })
};

module.exports = authValidator;

16. src/validators/product.validator.js

javascript
const Joi = require('joi');
const constants = require('../config/constants');

const productValidator = {
  // Create product validation schema
  create: Joi.object({
    title: Joi.string()
      .min(3)
      .max(constants.VALIDATION.PRODUCT_TITLE_MAX_LENGTH)
      .required()
      .messages({
        'string.empty': 'Product title is required',
        'string.min': 'Product title must be at least 3 characters',
        'string.max': `Product title must not exceed ${constants.VALIDATION.PRODUCT_TITLE_MAX_LENGTH} characters`
      }),
    
    description: Joi.string()
      .min(10)
      .max(constants.VALIDATION.PRODUCT_DESC_MAX_LENGTH)
      .required()
      .messages({
        'string.empty': 'Product description is required',
        'string.min': 'Product description must be at least 10 characters',
        'string.max': `Product description must not exceed ${constants.VALIDATION.PRODUCT_DESC_MAX_LENGTH} characters`
      }),
    
    price: Joi.number()
      .positive()
      .required()
      .messages({
        'number.base': 'Price must be a number',
        'number.positive': 'Price must be greater than 0'
      }),
    
    stock: Joi.number()
      .integer()
      .min(0)
      .default(0)
      .messages({
        'number.base': 'Stock must be a number',
        'number.integer': 'Stock must be an integer',
        'number.min': 'Stock cannot be negative'
      }),
    
    category: Joi.string()
      .valid(...Object.values(constants.PRODUCT_CATEGORIES))
      .required()
      .messages({
        'any.only': 'Invalid product category',
        'string.empty': 'Product category is required'
      }),
    
    images: Joi.array()
      .items(Joi.string().uri())
      .default([])
      .messages({
        'array.base': 'Images must be an array',
        'string.uri': 'Each image must be a valid URL'
      })
  }),
  
  // Update product validation schema
  update: Joi.object({
    title: Joi.string()
      .min(3)
      .max(constants.VALIDATION.PRODUCT_TITLE_MAX_LENGTH)
      .messages({
        'string.min': 'Product title must be at least 3 characters',
        'string.max': `Product title must not exceed ${constants.VALIDATION.PRODUCT_TITLE_MAX_LENGTH} characters`
      }),
    
    description: Joi.string()
      .min(10)
      .max(constants.VALIDATION.PRODUCT_DESC_MAX_LENGTH)
      .messages({
        'string.min': 'Product description must be at least 10 characters',
        'string.max': `Product description must not exceed ${constants.VALIDATION.PRODUCT_DESC_MAX_LENGTH} characters`
      }),
    
    price: Joi.number()
      .positive()
      .messages({
        'number.base': 'Price must be a number',
        'number.positive': 'Price must be greater than 0'
      }),
    
    stock: Joi.number()
      .integer()
      .min(0)
      .messages({
        'number.base': 'Stock must be a number',
        'number.integer': 'Stock must be an integer',
        'number.min': 'Stock cannot be negative'
      }),
    
    category: Joi.string()
      .valid(...Object.values(constants.PRODUCT_CATEGORIES))
      .messages({
        'any.only': 'Invalid product category'
      }),
    
    images: Joi.array()
      .items(Joi.string().uri())
      .messages({
        'array.base': 'Images must be an array',
        'string.uri': 'Each image must be a valid URL'
      }),
    
    isActive: Joi.boolean()
      .messages({
        'boolean.base': 'isActive must be a boolean'
      })
  }).min(1) // At least one field must be provided for update
};

module.exports = productValidator;

17. src/middleware/validation.middleware.js

javascript
const { apiError } = require('../utils/apiResponse');
const constants = require('../config/constants');
const logger = require('../utils/logger');

/**
 * Middleware untuk validasi request body menggunakan Joi schema
 */
const validate = (schema, property = 'body') => {
  return (req, res, next) => {
    try {
      logger.info('Validation middleware: Validating request', {
        path: req.path,
        property: property
      });
      
      const { error, value } = schema.validate(req[property], {
        abortEarly: false, // Return all errors, not just the first one
        stripUnknown: true // Remove unknown fields
      });
      
      if (error) {
        logger.warn('Validation middleware: Validation failed', {
          errors: error.details,
          path: req.path
        });
        
        // Format error response
        const errors = error.details.map(detail => ({
          field: detail.path.join('.'),
          message: detail.message.replace(/"/g, ''),
          type: detail.type
        }));
        
        return apiError(
          res,
          constants.HTTP_STATUS.BAD_REQUEST,
          'Validation Error',
          'VALIDATION_FAILED',
          { errors }
        );
      }
      
      // Replace request data with validated data
      req[property] = value;
      
      logger.info('Validation middleware: Validation passed');
      next();
    } catch (error) {
      logger.error('Validation middleware error:', error);
      next(error);
    }
  };
};

/**
 * Middleware untuk validasi query parameters
 */
const validateQuery = (schema) => validate(schema, 'query');

/**
 * Middleware untuk validasi route parameters
 */
const validateParams = (schema) => validate(schema, 'params');

module.exports = {
  validate,
  validateQuery,
  validateParams
};

18. src/middleware/auth.middleware.js

javascript
const { apiError } = require('../utils/apiResponse');
const constants = require('../config/constants');
const logger = require('../utils/logger');

/**
 * Middleware untuk verifikasi JWT token
 */
const authenticate = (req, res, next) => {
  try {
    logger.info('Auth middleware: Authenticating request');
    
    const authHeader = req.headers.authorization;
    
    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      logger.warn('Auth middleware: No token provided');
      return apiError(
        res,
        constants.HTTP_STATUS.UNAUTHORIZED,
        'Authentication required',
        'NO_TOKEN_PROVIDED'
      );
    }
    
    const token = authHeader.split(' ')[1];
    
    // Simulate token verification
    // In real app, use jsonwebtoken.verify()
    try {
      const tokenPayload = JSON.parse(
        Buffer.from(token.split('_')[1], 'base64').toString()
      );
      
      // Check token expiration (simplified)
      const tokenAge = Date.now() - tokenPayload.iat;
      const maxAge = 7 * 24 * 60 * 60 * 1000; // 7 days in milliseconds
      
      if (tokenAge > maxAge) {
        logger.warn('Auth middleware: Token expired');
        return apiError(
          res,
          constants.HTTP_STATUS.UNAUTHORIZED,
          'Token expired',
          'TOKEN_EXPIRED'
        );
      }
      
      // Attach user info to request
      req.user = {
        id: tokenPayload.sub,
        role: tokenPayload.role
      };
      
      logger.info(`Auth middleware: User authenticated - ID: ${req.user.id}, Role: ${req.user.role}`);
      next();
    } catch (tokenError) {
      logger.warn('Auth middleware: Invalid token');
      return apiError(
        res,
        constants.HTTP_STATUS.UNAUTHORIZED,
        'Invalid token',
        'INVALID_TOKEN'
      );
    }
  } catch (error) {
    logger.error('Auth middleware error:', error);
    next(error);
  }
};

/**
 * Middleware untuk authorization berdasarkan role
 */
const authorize = (...roles) => {
  return (req, res, next) => {
    try {
      logger.info('Authorization middleware: Checking permissions', { 
        requiredRoles: roles,
        userRole: req.user?.role 
      });
      
      if (!req.user) {
        logger.warn('Authorization middleware: No user found in request');
        return apiError(
          res,
          constants.HTTP_STATUS.UNAUTHORIZED,
          'Authentication required',
          'NOT_AUTHENTICATED'
        );
      }
      
      if (!roles.includes(req.user.role)) {
        logger.warn(`Authorization middleware: User ${req.user.id} with role ${req.user.role} not authorized`);
        return apiError(
          res,
          constants.HTTP_STATUS.FORBIDDEN,
          'Insufficient permissions',
          'INSUFFICIENT_PERMISSIONS',
          {
            requiredRoles: roles,
            userRole: req.user.role
          }
        );
      }
      
      logger.info(`Authorization middleware: User ${req.user.id} authorized`);
      next();
    } catch (error) {
      logger.error('Authorization middleware error:', error);
      next(error);
    }
  };
};

module.exports = {
  authenticate,
  authorize
};

19. src/middleware/error.middleware.js

javascript
const { apiError } = require('../utils/apiResponse');
const constants = require('../config/constants');
const logger = require('../utils/logger');

/**
 * Global error handling middleware
 */
const errorHandler = (err, req, res, next) => {
  logger.error('Global error handler:', {
    error: err.message,
    stack: err.stack,
    path: req.path,
    method: req.method,
    user: req.user?.id
  });
  
  // Handle known error types
  if (err.statusCode && err.errorCode) {
    return apiError(
      res,
      err.statusCode,
      err.message,
      err.errorCode,
      err.details
    );
  }
  
  // Handle Joi validation errors
  if (err.name === 'ValidationError') {
    return apiError(
      res,
      constants.HTTP_STATUS.BAD_REQUEST,
      'Validation Error',
      'VALIDATION_ERROR',
      { errors: err.details }
    );
  }
  
  // Handle duplicate key error (simulated)
  if (err.code === 'EMAIL_ALREADY_EXISTS') {
    return apiError(
      res,
      constants.HTTP_STATUS.CONFLICT,
      err.message,
      'DUPLICATE_ENTRY',
      err.details
    );
  }
  
  // Default error
  return apiError(
    res,
    constants.HTTP_STATUS.INTERNAL_SERVER_ERROR,
    process.env.NODE_ENV === 'development' ? err.message : 'Internal server error',
    'INTERNAL_ERROR',
    process.env.NODE_ENV === 'development' ? { stack: err.stack } : null
  );
};

/**
 * 404 Not Found middleware
 */
const notFound = (req, res, next) => {
  logger.warn('404 Not Found:', {
    path: req.originalUrl,
    method: req.method,
    ip: req.ip
  });
  
  return apiError(
    res,
    constants.HTTP_STATUS.NOT_FOUND,
    `Cannot ${req.method} ${req.originalUrl}`,
    'ROUTE_NOT_FOUND'
  );
};

module.exports = {
  errorHandler,
  notFound
};

20. src/controllers/auth.controller.js

javascript
const authService = require('../services/auth.service');
const { apiResponse } = require('../utils/apiResponse');
const { asyncHandler } = require('../utils/asyncHandler');
const logger = require('../utils/logger');

const authController = {
  /**
   * Register new user
   */
  register: asyncHandler(async (req, res) => {
    logger.info('AuthController.register: Processing registration request');
    
    const result = await authService.register(req.body);
    
    logger.info(`AuthController.register: User registered successfully - ID: ${result.user.id}`);
    
    return apiResponse(
      res,
      201,
      result,
      'User registered successfully'
    );
  }),
  
  /**
   * Login user
   */
  login: asyncHandler(async (req, res) => {
    logger.info('AuthController.login: Processing login request');
    
    const { email, password } = req.body;
    const result = await authService.login(email, password);
    
    logger.info(`AuthController.login: User logged in successfully - ID: ${result.user.id}`);
    
    return apiResponse(
      res,
      200,
      result,
      'Login successful'
    );
  }),
  
  /**
   * Get user profile
   */
  getProfile: asyncHandler(async (req, res) => {
    logger.info(`AuthController.getProfile: Getting profile for user ${req.user.id}`);
    
    const profile = await authService.getProfile(req.user.id);
    
    logger.info(`AuthController.getProfile: Profile retrieved for user ${req.user.id}`);
    
    return apiResponse(
      res,
      200,
      profile,
      'Profile retrieved successfully'
    );
  }),
  
  /**
   * Update user profile
   */
  updateProfile: asyncHandler(async (req, res) => {
    logger.info(`AuthController.updateProfile: Updating profile for user ${req.user.id}`);
    
    const updatedProfile = await authService.updateProfile(req.user.id, req.body);
    
    logger.info(`AuthController.updateProfile: Profile updated for user ${req.user.id}`);
    
    return apiResponse(
      res,
      200,
      updatedProfile,
      'Profile updated successfully'
    );
  })
};

module.exports = authController;

21. src/controllers/product.controller.js

javascript
const productService = require('../services/product.service');
const { apiResponse } = require('../utils/apiResponse');
const { asyncHandler } = require('../utils/asyncHandler');
const logger = require('../utils/logger');

const productController = {
  /**
   * Get all products
   */
  getProducts: asyncHandler(async (req, res) => {
    logger.info('ProductController.getProducts: Fetching products');
    
    const result = await productService.getProducts(req.query);
    
    logger.info(`ProductController.getProducts: Retrieved ${result.data.length} products`);
    
    return apiResponse(
      res,
      200,
      result.data,
      'Products retrieved successfully',
      {
        pagination: result.pagination,
        filters: result.filters
      }
    );
  }),
  
  /**
   * Get product by ID
   */
  getProductById: asyncHandler(async (req, res) => {
    const { id } = req.params;
    logger.info(`ProductController.getProductById: Fetching product ${id}`);
    
    const product = await productService.getProductById(id);
    
    logger.info(`ProductController.getProductById: Product ${id} retrieved`);
    
    return apiResponse(
      res,
      200,
      product,
      'Product retrieved successfully'
    );
  }),
  
  /**
   * Create new product
   */
  createProduct: asyncHandler(async (req, res) => {
    logger.info(`ProductController.createProduct: Creating product for seller ${req.user.id}`);
    
    const product = await productService.createProduct(req.body, req.user.id);
    
    logger.info(`ProductController.createProduct: Product created with ID ${product.id}`);
    
    return apiResponse(
      res,
      201,
      product,
      'Product created successfully'
    );
  }),
  
  /**
   * Update product
   */
  updateProduct: asyncHandler(async (req, res) => {
    const { id } = req.params;
    logger.info(`ProductController.updateProduct: Updating product ${id}`);
    
    const product = await productService.updateProduct(id, req.body, req.user.id);
    
    logger.info(`ProductController.updateProduct: Product ${id} updated`);
    
    return apiResponse(
      res,
      200,
      product,
      'Product updated successfully'
    );
  }),
  
  /**
   * Delete product
   */
  deleteProduct: asyncHandler(async (req, res) => {
    const { id } = req.params;
    logger.info(`ProductController.deleteProduct: Deleting product ${id}`);
    
    const result = await productService.deleteProduct(id, req.user.id);
    
    logger.info(`ProductController.deleteProduct: Product ${id} deleted`);
    
    return apiResponse(
      res,
      200,
      result,
      'Product deleted successfully'
    );
  })
};

module.exports = productController;

22. src/routes/auth.routes.js

javascript
const express = require('express');
const router = express.Router();
const authController = require('../controllers/auth.controller');
const authValidator = require('../validators/auth.validator');
const { validate } = require('../middleware/validation.middleware');
const { authenticate } = require('../middleware/auth.middleware');

// Public routes
router.post(
  '/register',
  validate(authValidator.register),
  authController.register
);

router.post(
  '/login',
  validate(authValidator.login),
  authController.login
);

// Protected routes
router.get(
  '/profile',
  authenticate,
  authController.getProfile
);

router.patch(
  '/profile',
  authenticate,
  validate(authValidator.updateProfile),
  authController.updateProfile
);

module.exports = router;

23. src/routes/product.routes.js

javascript
const express = require('express');
const router = express.Router();
const productController = require('../controllers/product.controller');
const productValidator = require('../validators/product.validator');
const { validate, validateQuery } = require('../middleware/validation.middleware');
const { authenticate, authorize } = require('../middleware/auth.middleware');
const constants = require('../config/constants');

// Public routes
router.get(
  '/',
  validateQuery(productValidator.create), // Reuse create schema for query validation
  productController.getProducts
);

router.get(
  '/:id',
  productController.getProductById
);

// Protected routes (seller only)
router.post(
  '/',
  authenticate,
  authorize(constants.USER_ROLES.SELLER, constants.USER_ROLES.ADMIN),
  validate(productValidator.create),
  productController.createProduct
);

router.patch(
  '/:id',
  authenticate,
  authorize(constants.USER_ROLES.SELLER, constants.USER_ROLES.ADMIN),
  validate(productValidator.update),
  productController.updateProduct
);

router.delete(
  '/:id',
  authenticate,
  authorize(constants.USER_ROLES.SELLER, constants.USER_ROLES.ADMIN),
  productController.deleteProduct
);

module.exports = router;

24. src/routes/index.js

javascript
const express = require('express');
const router = express.Router();
const config = require('../config/env');
const authRoutes = require('./auth.routes');
const productRoutes = require('./product.routes');
const { apiResponse } = require('../utils/apiResponse');

// Health check endpoint
router.get('/health', (req, res) => {
  apiResponse(res, 200, {
    status: 'healthy',
    timestamp: new Date().toISOString(),
    service: config.APP_NAME,
    version: config.APP_VERSION,
    environment: config.NODE_ENV
  }, 'Service is healthy');
});

// API documentation
router.get('/', (req, res) => {
  apiResponse(res, 200, {
    service: config.APP_NAME,
    version: config.APP_VERSION,
    documentation: `${req.protocol}://${req.get('host')}/docs`,
    endpoints: {
      auth: `${config.API_PREFIX}/auth`,
      products: `${config.API_PREFIX}/products`,
      health: `${config.API_PREFIX}/health`
    }
  }, 'Welcome to E-commerce API');
});

// Mount routes
router.use('/auth', authRoutes);
router.use('/products', productRoutes);

module.exports = router;

25. src/app.js

javascript
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
const config = require('./config/env');
const logger = require('./utils/logger');
const { notFound, errorHandler } = require('./middleware/error.middleware');
const apiRoutes = require('./routes/index');

const app = express();

// ==================== MIDDLEWARE SETUP ====================

// Security middleware
app.use(helmet());

// CORS configuration
app.use(cors({
  origin: config.CORS_ORIGIN,
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization']
}));

// Request logging
app.use(morgan('combined', {
  stream: {
    write: (message) => logger.http(message.trim())
  }
}));

// Body parsing middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// ==================== ROUTES SETUP ====================

// API routes
app.use(config.API_PREFIX, apiRoutes);

// ==================== ERROR HANDLING ====================

// 404 handler
app.use(notFound);

// Global error handler
app.use(errorHandler);

module.exports = app;

26. src/server.js

javascript
const app = require('./app');
const config = require('./config/env');
const logger = require('./utils/logger');

const PORT = config.PORT || 3000;

const server = app.listen(PORT, () => {
  logger.info(`
╔══════════════════════════════════════════════════════════╗
║                🚀 E-commerce API Started                 ║
╠══════════════════════════════════════════════════════════╣
║ Service: ${config.APP_NAME}
║ Version: ${config.APP_VERSION}
║ Environment: ${config.NODE_ENV}
║ Port: ${PORT}
║ API: http://localhost:${PORT}${config.API_PREFIX}
║ Health: http://localhost:${PORT}${config.API_PREFIX}/health
╚══════════════════════════════════════════════════════════╝
  `);
});

// Graceful shutdown
process.on('SIGTERM', () => {
  logger.info('SIGTERM received. Shutting down gracefully...');
  server.close(() => {
    logger.info('Process terminated');
  });
});

process.on('SIGINT', () => {
  logger.info('SIGINT received. Shutting down gracefully...');
  server.close(() => {
    logger.info('Process terminated');
  });
});

// Handle unhandled promise rejections
process.on('unhandledRejection', (reason, promise) => {
  logger.error('Unhandled Rejection at:', promise, 'reason:', reason);
});

// Handle uncaught exceptions
process.on('uncaughtException', (error) => {
  logger.error('Uncaught Exception:', error);
  process.exit(1);
});

module.exports = server;

3.5 Flow Data dalam Struktur Rapi

javascript
// 📊 FLOW DATA: Client → Server → Database → Client

// 1. CLIENT mengirim request
//    POST /api/v1/products
//    Headers: { Authorization: Bearer token }
//    Body: { title: "Laptop", price: 10000000, ... }

// 2. ROUTES (src/routes/product.routes.js)
//    - Validasi path & method
//    - Terapkan middleware: auth, validation
//    - Panggil controller

// 3. MIDDLEWARE (src/middleware/)
//    - auth.middleware.js: Cek token
//    - validation.middleware.js: Validasi data

// 4. VALIDATORS (src/validators/product.validator.js)
//    - Validasi schema menggunakan Joi
//    - Return error jika invalid

// 5. CONTROLLERS (src/controllers/product.controller.js)
//    - Terima request yang sudah divalidasi
//    - Panggil service layer
//    - Format response

// 6. SERVICES (src/services/product.service.js)
//    - Business logic
//    - Validasi business rules
//    - Panggil repository layer

// 7. REPOSITORIES (src/repositories/product.repository.js)
//    - Data access logic
//    - Query ke database
//    - Return data

// 8. MODELS (src/models/Product.model.js)
//    - Data structure definition
//    - Business entity representation

// 9. RESPONSE kembali ke CLIENT
//    - Format response konsisten
//    - Status code yang tepat
//    - Data yang sudah diproses

3.6 Keuntungan Struktur Rapi

1. Separation of Concerns

javascript
// Setiap layer punya tanggung jawab tunggal:
// - Routes: Routing & middleware composition
// - Controllers: Request/response handling
// - Services: Business logic
// - Repositories: Data access
// - Models: Data structure

2. Testability

javascript
// Mudah testing tiap komponen:
test('ProductService.createProduct should validate price', () => {
  // Test service logic tanpa HTTP
});

test('ProductController should return 201 on success', () => {
  // Test controller response
});

3. Maintainability

javascript
// Perubahan di satu layer tidak pengaruh layer lain:
// Ubah database? Cuma ubah repository
// Ubah business logic? Cuma ubah service
// Ubah API response? Cuma ubah controller

4. Team Collaboration

javascript
// Multiple developers bisa kerja parallel:
// Developer A: Kerja di auth feature
// Developer B: Kerja di product feature
// Developer C: Kerja di payment feature
// Tidak ada conflict karena file terpisah

5. Scalability

javascript
// Mudah scale:
// - Split monolith ke microservices
// - Add new features tanpa ganggu existing
// - Replace components (e.g., database driver)

3.7 Best Practices Struktur Project

1. Naming Convention Konsisten

javascript
// ✅ Good
user.controller.js
user.service.js
user.repository.js
user.model.js
user.validator.js
user.routes.js

// ❌ Bad
userCtrl.js
UserService.js
userRepo.js
UserModel.js
validateUser.js
userRouter.js

2. File Size Management

javascript
// Jika file terlalu besar (> 300 lines), split:
// ❌ Bad: user.service.js (1000 lines)
// ✅ Good:
//   user.service.js (base class)
//   user.auth.service.js (auth logic)
//   user.profile.service.js (profile logic)
//   user.password.service.js (password logic)

3. Dependency Injection Pattern

javascript
// Gunakan dependency injection untuk loose coupling
class ProductService {
  constructor(productRepository, logger) {
    this.repository = productRepository;
    this.logger = logger;
  }
  
  async getProducts() {
    return this.repository.findAll();
  }
}

// Testing mudah dengan mock
const mockRepo = { findAll: jest.fn() };
const service = new ProductService(mockRepo, console);

4. Barrel Exports

javascript
// Di setiap folder, buat index.js untuk export
// src/services/index.js:
module.exports = {
  authService: require('./auth.service'),
  productService: require('./product.service'),
  orderService: require('./order.service')
};

// Penggunaan:
const { authService, productService } = require('../services');

3.8 Common Anti-Patterns to Avoid

1. God Objects

javascript
// ❌ Bad: Semua logic di satu file
// app.js (1000+ lines)
// - Routes
// - Controllers
// - Services
// - Database
// - Middleware
// - Config

// ✅ Good: Separation of concerns
// Setiap concern di file terpisah

2. Tight Coupling

javascript
// ❌ Bad: Controller langsung akses database
app.post('/products', (req, res) => {
  const db = require('./database');
  db.query('INSERT...'); // Direct database access
});

// ✅ Good: Layer separation
// Controller → Service → Repository → Database

3. Business Logic in Controllers

javascript
// ❌ Bad: Business logic di controller
app.post('/orders', (req, res) => {
  // Price calculation
  // Inventory check
  // Payment processing
  // Email notification
  // Semua di controller!
});

// ✅ Good: Business logic di service layer
orderService.processOrder(orderData);

3.9 Tools untuk Maintain Structure

1. ESLint & Prettier - Code consistency
2. Husky & lint-staged - Pre-commit hooks
3. Jest - Testing framework
4. Docker - Consistent environment
5. CI/CD Pipeline - Automated testing & deployment

3.10 Latihan Praktikum

Exercise 1: Refactor Monolith ke Structured

javascript
// Diberikan file server.js monolith (500 lines),
// refactor ke struktur rapi dengan:
// 1. Routes terpisah
// 2. Controllers terpisah
// 3. Services terpisah
// 4. Models terpisah

Exercise 2: Add New Feature

javascript
// Tambahkan fitur "Wishlist" ke e-commerce API:
// 1. Buat Wishlist model
// 2. Buat Wishlist repository
// 3. Buat Wishlist service
// 4. Buat Wishlist controller
// 5. Buat Wishlist routes
// 6. Buat Wishlist validators

Exercise 3: Implement Caching Layer

javascript
// Tambahkan caching layer antara service dan repository:
// 1. Buat CacheService
// 2. Modify repositories untuk check cache dulu
// 3. Implement cache invalidation
// 4. Add cache configuration

4. Daftar Pustaka

  1. Node.js Best Practices (2024). Project Structure. Diakses dari https://github.com/goldbergyoni/nodebestpractices
  2. Clean Architecture by Robert C. Martin (2017). Book on software architecture
  3. Express.js Application Structure (2024). Official Patterns. Diakses dari https://expressjs.com/en/advanced/best-practice-performance.html
  4. SOLID Principles (2024). Object-Oriented Design. Diakses dari https://www.digitalocean.com/community/conceptual_articles/s-o-l-i-d-the-first-five-principles-of-object-oriented-design
  5. Microservices vs Monolith (2024). Architecture Patterns. Diakses dari https://microservices.io/

Posting Komentar

0 Komentar