Skip to main content

Naming Conventions

Files and Directories

  • Routes: Use kebab-case for route files: user-profiles/routes.ts
  • Services: Use PascalCase for service files: UserProfileService.ts
  • Directories: Use kebab-case: user-api/, merchant-profiles/
  • API Endpoints: Use kebab-case in URLs: /user-profiles/, /merchant-settings/

Code Elements

  • Classes: PascalCase - UserService, ProductCatalogService
  • Methods: camelCase - getUserProfile(), createProduct()
  • Variables: camelCase - userId, productData, isAuthenticated
  • Constants: UPPER_SNAKE_CASE - DATABASE_URL, JWT_SECRET
  • Interfaces/Types: PascalCase - DBConnection, ApiRequest

Database Schema

  • Tables: snake_case - user_profiles, product_categories
  • Columns: snake_case - user_id, created_at, merchant_id
  • Multi-tenancy: Always include shop_id or merchant_id

Import Organization

Organize imports in the following order with blank lines between groups:
// 1. Standard library imports
import { Router } from "express";
import { body as checkBody } from "express-validator";

// 2. Core modules (using 'core' alias)
import APIService from "core/base/service";
import { DBConnection, ID } from "core/types";
import { authenticateUser } from "core/auth/middlewares";

// 3. Shared libraries (using 'lib' alias)
import { CustomError } from "lib/errors/custom_error";
import kv from "lib/connectors/redis";

// 4. Local modules (relative imports)
import SomeService from "./service";
import { validateInput } from "../utils/validation";

Module Structure

Service Class Pattern

All service classes must extend APIService and follow this pattern:
import APIService from "core/base/service";
import { DBConnection, ID } from "core/types";

export default class ModuleService extends APIService {
  constructor(db: DBConnection) {
    super(db, "table_name");
  }

  async list(filters = {}, pagination = {}) {
    return await this.findForList(filters, pagination);
  }

  async detail(id: ID) {
    return await this.findOneOrThrow(id);
  }

  async create(data: any) {
    // Validation logic
    return await this.create(data);
  }
}

Route Handler Pattern

Route files must export a function that accepts DBConnection:
import { Router } from "express";
import { body as checkBody } from "express-validator";
import ModuleService from "./service";
import { DBConnection } from "core/types";

export default (db: DBConnection) => {
  const routes: any = Router();
  const service = new ModuleService(db);

  routes.get(
    "/list",
    service.handleOk(
      async (req) =>
        await service.list(req.query, {
          page: parseInt(req.query.page as string) || 1,
          limit: parseInt(req.query.limit as string) || 50
        })
    )
  );

  return routes;
};

Error Handling

Custom Error Usage

Always use CustomError from the shared library:
import { CustomError } from "lib/errors/custom_error";

// In service methods
if (!user) {
  throw new CustomError("USER_NOT_FOUND", "User not found with given ID", {
    userId: id
  });
}

if (!req.currentUser.is_super) {
  throw new CustomError("FORBIDDEN", "Super admin access required");
}

Response Handling

Use the service.handleOk() pattern for consistent responses:
routes.post(
  "/create",
  [checkBody("name").notEmpty()],
  service.handleOk(async (req) => {
    const result = await service.create(req.body);
    return { message: "Created successfully", data: result };
  })
);

Database Access

Multi-tenancy Pattern

Always filter by merchant/shop ID for data isolation:
async getUsersByShop(shopId: ID) {
  return await this.connector.db("users")
    .where("shop_id", shopId)
    .select("*");
}

async createProduct(data: any, merchantId: ID) {
  return await this.connector.db("products")
    .insert({
      ...data,
      shop_id: merchantId,
      created_at: new Date()
    });
}

Query Patterns

Use Knex.js patterns consistently:
// List with pagination
async findForList(filters = {}, pagination = { page: 1, limit: 50 }) {
  const query = this.connector.db(this.tableName);

  // Apply filters
  if (filters.status) {
    query.where("status", filters.status);
  }

  // Apply pagination
  const offset = (pagination.page - 1) * pagination.limit;
  query.offset(offset).limit(pagination.limit);

  return await query;
}

Authentication & Authorization

Request Context Usage

Access user context from authenticated requests:
routes.get(
  "/profile",
  service.handleOk(async (req) => {
    // User data available after authentication
    const userId = req.currentUser.id;
    const merchantId = req.merchant_id;

    return await service.getUserProfile(userId, merchantId);
  })
);

Role-based Access

Check permissions using user context:
// Check super admin status
if (!req.currentUser.is_super) {
  throw new CustomError("FORBIDDEN", "Super admin required");
}

// Check specific roles
const hasPermission = req.currentUser.roles.includes("merchant_manager");
if (!hasPermission) {
  throw new CustomError("FORBIDDEN", "Insufficient permissions");
}

Event Publishing

Domain Events

Publish events for important domain actions:
async createOrder(orderData: any) {
  const order = await this.create(orderData);

  // Publish domain event
  await this.publishEvent("order.created", {
    orderId: order.id,
    merchantId: order.merchant_id,
    amount: order.total,
    timestamp: new Date()
  });

  return order;
}

TypeScript Usage

Type Safety

Use proper typing throughout:
import { ID, ApiRequest } from "core/types";

interface CreateProductRequest {
  name: string;
  price: number;
  category_id: ID;
  merchant_id: ID;
}

async createProduct(data: CreateProductRequest): Promise<Product> {
  return await this.create(data);
}

Path Aliases

Use configured path aliases for imports:
// Use 'core' alias for @core modules
import APIService from "core/base/service";
import { DBConnection } from "core/types";

// Use 'lib' alias for lib modules
import { CustomError } from "lib/errors/custom_error";
import kv from "lib/connectors/redis";