Skip to content

Code Standards & Guidelines

Project: PTX Channel Manager (ptx-cm)
Version: 3.0.5
Last Updated: 2026-06-05


1. Project Principles

YAGNI - You Aren't Gonna Need It
Don't build features before they're required. Avoid over-engineering.

KISS - Keep It Simple, Stupid
Prefer straightforward solutions over clever abstractions.

DRY - Don't Repeat Yourself
Extract common logic into helpers, services, or utilities.


2. Naming Conventions

Files & Directories

ContextPatternExample
TypeScript/JavaScriptkebab-casecountry-scope.guard.ts, api-client.ts
Directorieskebab-caseota-accounts/, room-mappings/
Test filesname.spec.tsauth.service.spec.ts
DTOsPascalCase + Dto suffixCreatePropertyDto, ListBookingsDto
InterfacesPascalCase + optional I prefixOtaAdapter or IOtaAdapter
ClassesPascalCaseAuthService, PropertyController
EnumsPascalCaseUserRole, AlertSeverity
ConstantsUPPER_SNAKE_CASEALLOWED_COUNTRIES, JWT_EXPIRY
Database tablessnake_case (Prisma)ota_accounts, refresh_tokens
Database columnssnake_casepassword_hash, is_active

Functions & Methods

typescript
// ✓ Good: Verb + noun, descriptive
async fetchBookingsFromOta(otaType: string): Promise<Booking[]>
function assertPropertyAccess(prisma, propertyId, countryScope?): Property
function validateSortByField(field: string, allowedFields: string[]): void

// ✗ Bad: Abbreviations, unclear purpose
async getB(type: string): Promise<any>
function checkProp(id: string): void

Variables

typescript
// ✓ Good: Descriptive, no unnecessary abbreviations
const refreshTokenExpiry = '7d';
const MAX_RETRY_ATTEMPTS = 3;
let availableRooms = totalRooms - bookedRooms;

// ✗ Bad: Single letter, abbreviations
const rt = '7d';
const max_retries = 3;
let avail = total - booked;

3. File Organization & Size Limits

Code Files: Max 200 Lines

Goal: Optimize for LLM context windows and developer comprehension.

Split Strategy:

  • 200-400 lines: Consider splitting into 2-3 focused files
  • 400+ lines: MUST split immediately

Example split for a large service:

ota-accounts/
├── ota-accounts.service.ts         # CRUD (150 lines)
├── ota-accounts.controller.ts      # Endpoints
├── ota-account-discovery.service.ts # Discovery & import logic (120 lines)
├── ota-credential.helper.ts        # Encryption/decryption (80 lines)
├── dto/
│   ├── create-ota-account.dto.ts
│   └── list-ota-accounts.dto.ts
└── ota-accounts.module.ts

Documentation Files: Max 800 Lines

Goal: Keep docs maintainable and readable.

Split Strategy:

  • 800-1200 lines: Create topic directory with index + parts
  • 1200+ lines: MUST split

4. Entity URLs & Internal Codes

Principle: Entity detail URLs are the public identity; use short codes (e.g. CU-A3F92B, MY-22846F) for UIs and URLs, never raw UUIDs.

URL Builders

All entity detail links MUST be built via apps/web/lib/routes.ts typed builders — pass entity objects, never bare strings. TypeScript enforces this at compile time.

typescript
import { routes } from '@/lib/routes';

// ✓ CORRECT: builders take entity objects
router.push(routes.property({ internalCode: 'MY-22846F' }));
router.push(routes.customer({ internalCode: 'CU-A3F92B' }));
router.push(routes.booking({ internalCode: 'BK-7D21C4' }));
router.push(routes.supplierApartment({ internalCode: 'SA-1B0C44' }));
router.push(routes.supplier({ supplierCode: 'HCM001' }));
router.push(routes.department({ code: 'CS-001' }));

// ✗ NEVER: hardcoded strings or raw UUIDs
router.push(`/properties/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`);
router.push(`/customers/${uuid}`);

Display

  • Detail pages: Display internalCode (or supplierCode for suppliers) in page headers and breadcrumbs, never the entity's UUID.
  • List pages: Show internalCode as the entity's primary identifier column.
  • Links: Use internalCode when building copy-able links or mentions.

Route Parameter Handling

Detail routes (e.g. /properties/:id, /bookings/:id) accept either internal code OR UUID in the param:

GET /api/v1/properties/MY-22846F    ← internal code (preferred)
GET /api/v1/properties/xxxxxxxx...  ← UUID (legacy, always works)

The EntityCodeService (backend) resolves either to the entity's real UUID. Page templates canonicalize UUID params via router.replace() to the canonical code URL.

Request Body & Query Parameters

Anything sent in request body or query string MUST be the entity's real UUID (entity.id), never the route param or internal code:

typescript
// ✓ CORRECT: route has code, body has real UUID
PATCH /api/v1/bookings/BK-7D21C4
{ "assignedTo": "12345678-..." }

// ✗ WRONG: never send code in body
PATCH /api/v1/bookings/BK-7D21C4
{ "assignedTo": "BK-7D21C4" }

New Entity Checklist

When adding a detail page to a new entity, follow 4 mandatory steps:

  1. Database:

    • Add column: internalCode String @unique @map("internal_code") to Prisma schema
    • Create backfill migration (deterministic from UUID suffix; handle collisions with 8-hex variant)
  2. Types (packages/types):

    • If entity uses a fixed 2-letter prefix, add to ENTITY_CODE_PREFIXES in entity-codes.ts (e.g., customer: 'CU')
    • Properties use country code dynamically (e.g., MY, TH) — no entry needed
  3. Backend (apps/api):

    • Add case to EntityCodeService.resolve() switch statement for route-param resolution
    • Add case to EntityCodeService.generateFor() generator (if you create/clone entities)
    • Call service.generateFor(entity, prefix) at every entity creation site (controllers)
  4. Frontend (apps/web):

    • Add builder to lib/routes.ts:
      typescript
      newEntity: (e: { internalCode: string }) => `/new-entities/${e.internalCode}`,
    • Replace all UUID displays with internalCode in templates

Verification Checklist

After deploying, confirm no UUID leakage with these greps (should return 0 matches):

bash
# No hardcoded URL templates with raw route params
grep -rn '/properties/\${\|/customers/\${\|/suppliers/\${\|/bookings/\${\|/supplier-apartments/\${' \
  apps/web/app apps/web/components apps/web/lib \
  | grep -v '.test.' | grep -v '/api/' | grep -v 'lib/routes'

# No code displays of entity.id (should display entity.internalCode)
grep -rn "id.slice(\|.id.replace(/-/g\|\.id}" apps/web/app apps/web/components \
  | grep -v '.test.' | grep -v 'id =' | grep -v 'id:' | grep -v '{id}'

DocumentContents
code-standards-backend.mdNestJS patterns, security, error handling, DB/Prisma, testing
code-standards-frontend.mdNext.js/React patterns, API client, Git commits, performance
system-architecture.mdArchitecture diagrams and flows
codebase-summary.mdProject structure and dependencies
API_SPEC.mdREST API endpoint reference
DB_DESIGN.mdDatabase schema and indexes

PTX Channel Manager — Internal Documentation