Skip to main content

Module Structure

A module is the basic building block of the Zochil API. Each module represents a specific business domain and contains two essential files: routes.ts and service.ts.

Module Pattern

domain_module/
├── routes.ts    # Route definitions and Express router
└── service.ts   # Business logic and data access

Routes (routes.ts)

The routes.ts file creates and exports an Express Router. It handles HTTP route definitions, request validation, and middleware integration.

Example Route File

import { Router } from "express";
import { body as checkBody } from "express-validator";
import SomeService from "./service";
import { DBConnection } from "core/types";

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

  routes.get(
    "/list",
    service.handleOk(async (req) => await service.getAll(req.query))
  );

  routes.post(
    "/create",
    [checkBody("name").notEmpty()],
    service.handleOk(async (req) => await service.createRecord(req.body))
  );

  return routes;
};

Route Responsibilities

  • Express Router: Create and configure HTTP routes
  • Validation: Input validation with express-validator
  • Authentication: Apply authentication middleware
  • Response Handling: Use service.handleOk() for consistent responses
  • Dependency Injection: Receive DBConnection from parent router

Services (service.ts)

The service.ts file contains the business logic and data access layer. All service classes extend the base APIService class.

Example Service File

import APIService from "core/base/service";
import { DBConnection, ID } from "core/types";

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

  async getAll(filters = {}) {
    return await this.findForList(filters);
  }

  async createRecord(data: any) {
    return await this.create(data);
  }
}

Service Responsibilities

  • Business Logic: Core domain logic and data processing
  • Database Access: Query database using Knex.js patterns
  • Multi-tenancy: Automatic filtering by shop_id/merchant_id
  • Error Handling: Use CustomError for structured exceptions
  • Event Publishing: Publish domain events via this.publishEvent()

Base Classes

APIService Base Class

Located at ./api/@core/base/service.ts, provides common functionality:
import APIService from "core/base/service";

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

  // Inherited methods available:
  // - findForList(filters, pagination)
  // - findOneOrThrow(id)
  // - create(data)
  // - update(id, data)
  // - publishEvent(eventName, data)
}

Merchant Service Composition

For merchant-related functionality, use the MerchantService as a composed property:
import APIService from "core/base/service";
import MerchantService from "core/merchants/service";
import { DBConnection } from "core/types";

export default class ModuleWithMerchant extends APIService {
  private _merchantService: MerchantService;

  constructor(db: DBConnection) {
    super(db, "module_table_name");
    this._merchantService = new MerchantService(db, "shops");
  }

  async getMerchantData(merchantId: string) {
    return await this._merchantService.findOneOrThrow(merchantId);
  }
}

Database Connection

All modules receive a DBConnection interface containing:
export type DBConnection = {
  db: Knex; // PostgreSQL connection
  kv: RedisClient; // Redis connection
  paymentDB?: Knex; // Optional separate payment DB
};

Database Access Patterns

  • Knex Queries: Use this.connector.db(tableName) for database access
  • Multi-tenancy: Always filter by shop_id or merchant_id
  • Transactions: Use Knex transactions for complex operations
  • Connection Pooling: Automatic connection management

Authentication & Middleware

Request Context

Authentication middlewares populate request objects with user context:
// Available on authenticated requests
req.currentUser; // User data (excluding sensitive fields)
req.merchant_id; // Merchant ID from headers
req.device_id; // Device ID from headers

Authentication Middleware

import { authenticateUser, authenticateAdmin } from "core/auth/middlewares";

// In routes.ts
routes.use("*", authenticateUser(db)); // For user endpoints
routes.use("*", authenticateAdmin(db)); // For admin endpoints

Authorization

Role-based access control using user context:
// Check user roles
if (!req.currentUser.is_super) {
  throw new CustomError("FORBIDDEN", "Super admin required");
}

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