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
| Context | Pattern | Example |
|---|---|---|
| TypeScript/JavaScript | kebab-case | country-scope.guard.ts, api-client.ts |
| Directories | kebab-case | ota-accounts/, room-mappings/ |
| Test files | name.spec.ts | auth.service.spec.ts |
| DTOs | PascalCase + Dto suffix | CreatePropertyDto, ListBookingsDto |
| Interfaces | PascalCase + optional I prefix | OtaAdapter or IOtaAdapter |
| Classes | PascalCase | AuthService, PropertyController |
| Enums | PascalCase | UserRole, AlertSeverity |
| Constants | UPPER_SNAKE_CASE | ALLOWED_COUNTRIES, JWT_EXPIRY |
| Database tables | snake_case (Prisma) | ota_accounts, refresh_tokens |
| Database columns | snake_case | password_hash, is_active |
Functions & Methods
// ✓ 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): voidVariables
// ✓ 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.tsDocumentation 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.
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(orsupplierCodefor suppliers) in page headers and breadcrumbs, never the entity's UUID. - List pages: Show internalCode as the entity's primary identifier column.
- Links: Use
internalCodewhen 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:
// ✓ 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:
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)
- Add column:
Types (packages/types):
- If entity uses a fixed 2-letter prefix, add to
ENTITY_CODE_PREFIXESinentity-codes.ts(e.g.,customer: 'CU') - Properties use country code dynamically (e.g., MY, TH) — no entry needed
- If entity uses a fixed 2-letter prefix, add to
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)
- Add case to
Frontend (apps/web):
- Add builder to
lib/routes.ts:typescriptnewEntity: (e: { internalCode: string }) => `/new-entities/${e.internalCode}`, - Replace all UUID displays with
internalCodein templates
- Add builder to
Verification Checklist
After deploying, confirm no UUID leakage with these greps (should return 0 matches):
# 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}'5. Related Documentation
| Document | Contents |
|---|---|
| code-standards-backend.md | NestJS patterns, security, error handling, DB/Prisma, testing |
| code-standards-frontend.md | Next.js/React patterns, API client, Git commits, performance |
| system-architecture.md | Architecture diagrams and flows |
| codebase-summary.md | Project structure and dependencies |
| API_SPEC.md | REST API endpoint reference |
| DB_DESIGN.md | Database schema and indexes |