- Streamlined the README.md for clarity and conciseness. - Deleted outdated documentation files related to Freebit SIM management, SIM management API data flow, and various architectural guides to reduce clutter and improve maintainability. - Updated the last modified date in the README to reflect the latest changes.
11 KiB
11 KiB
Package Organization & "Lib" Files Strategy
Status: ✅ Implemented
Date: October 2025
🎯 Core Principle: Domain Package = Pure Domain Logic
The @customer-portal/domain package should contain ONLY pure domain logic that is:
- ✅ Framework-agnostic
- ✅ Reusable across frontend and backend
- ✅ Pure TypeScript (no React, no NestJS, no Next.js)
- ✅ No external infrastructure dependencies
📦 Package Structure Matrix
| Type | Location | Examples | Reasoning |
|---|---|---|---|
| Domain Types | packages/domain/*/contract.ts |
Invoice, Order, Customer |
Pure business entities |
| Validation Schemas | packages/domain/*/schema.ts |
invoiceSchema, orderQueryParamsSchema |
Runtime validation |
| Pure Utilities | packages/domain/toolkit/ |
formatCurrency(), parseDate() |
No framework dependencies |
| Provider Mappers | packages/domain/*/providers/ |
transformWhmcsInvoice() |
Data transformation logic |
| Framework Utils | apps/*/src/lib/ or apps/*/src/core/ |
API clients, React hooks | Framework-specific code |
| Shared Infra | packages/validation, packages/logging |
ZodPipe, useZodForm |
Framework bridges |
📂 Detailed Breakdown
✅ What Belongs in packages/domain/
1. Domain Types & Contracts
// packages/domain/billing/contract.ts
export interface Invoice {
id: number;
status: InvoiceStatus;
total: number;
}
2. Validation Schemas (Zod)
// packages/domain/billing/schema.ts
export const invoiceSchema = z.object({
id: z.number(),
status: invoiceStatusSchema,
total: z.number(),
});
// Domain-specific query params
export const invoiceQueryParamsSchema = z.object({
page: z.coerce.number().int().positive().optional(),
status: invoiceStatusSchema.optional(),
dateFrom: z.string().datetime().optional(),
});
3. Pure Utility Functions
// packages/domain/toolkit/formatting/currency.ts
export function formatCurrency(amount: number, currency: SupportedCurrency): string {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency,
}).format(amount);
}
✅ Pure function - no React, no DOM, no framework ✅ Business logic - directly related to domain entities ✅ Reusable - both frontend and backend can use it
4. Provider Mappers
// packages/domain/billing/providers/whmcs/mapper.ts
export function transformWhmcsInvoice(raw: WhmcsInvoiceRaw): Invoice {
return {
id: raw.invoiceid,
status: mapStatus(raw.status),
total: parseFloat(raw.total),
};
}
❌ What Should NOT Be in packages/domain/
1. Framework-Specific API Clients
// ❌ DO NOT put in domain
// apps/portal/src/lib/api/client.ts
import { ApiClient } from "@hey-api/client-fetch"; // ← Framework dependency
export const apiClient = new ApiClient({
baseUrl: process.env.NEXT_PUBLIC_API_URL, // ← Next.js specific
});
Why?
- Depends on
@hey-api/client-fetch(external library) - Uses Next.js environment variables
- Runtime infrastructure code
2. React Hooks
// ❌ DO NOT put in domain
// apps/portal/src/lib/hooks/useInvoices.ts
import { useQuery } from "@tanstack/react-query"; // ← React dependency
export function useInvoices() {
return useQuery({ ... }); // ← React-specific
}
Why? React-specific - backend can't use this
3. Error Handling with Framework Dependencies
// ❌ DO NOT put in domain
// apps/portal/src/lib/utils/error-handling.ts
import { ApiError as ClientApiError } from "@/lib/api"; // ← Framework client
export function getErrorInfo(error: unknown): ApiErrorInfo {
if (error instanceof ClientApiError) {
// ← Framework-specific error type
// ...
}
}
Why? Depends on the API client implementation
4. NestJS-Specific Utilities
// ❌ DO NOT put in domain
// apps/bff/src/core/utils/error.util.ts
export function getErrorMessage(error: unknown): string {
// Generic error extraction - could be in domain
if (error instanceof Error) {
return error.message;
}
return String(error);
}
This one is borderline - it's generic enough it COULD be in domain, but:
- Only used by backend
- Not needed for type definitions
- Better to keep application-specific utils in apps
🎨 Current Architecture (Correct!)
packages/
├── domain/ # ✅ Pure domain logic
│ ├── billing/
│ │ ├── contract.ts # ✅ Types
│ │ ├── schema.ts # ✅ Zod schemas + domain query params
│ │ └── providers/whmcs/ # ✅ Data mappers
│ ├── common/
│ │ ├── types.ts # ✅ Truly generic types (ApiResponse, PaginationParams)
│ │ └── schema.ts # ✅ Truly generic schemas (paginationParamsSchema, emailSchema)
│ └── toolkit/
│ ├── formatting/ # ✅ Pure functions (formatCurrency, formatDate)
│ └── validation/ # ✅ Pure validation helpers
│
├── validation/ # ✅ Framework bridges
│ ├── src/zod-pipe.ts # NestJS Zod pipe
│ └── src/zod-form.ts # React Zod form hook
│
└── logging/ # ✅ Infrastructure
└── src/logger.ts # Logging utilities
apps/
├── portal/ (Next.js)
│ └── src/lib/
│ ├── api/ # ❌ Framework-specific
│ │ ├── client.ts # API client instance
│ │ └── runtime/ # Generated client code
│ ├── hooks/ # ❌ React-specific
│ │ └── useAuth.ts # React Query hooks
│ └── utils/ # ❌ App-specific
│ ├── error-handling.ts # Portal error handling
│ └── cn.ts # Tailwind utility
│
└── bff/ (NestJS)
└── src/core/
├── validation/ # ❌ NestJS-specific
│ └── zod-validation.filter.ts
└── utils/ # ❌ App-specific
├── error.util.ts # BFF error utilities
└── validation.util.ts # BFF validation helpers
🔄 Decision Framework: "Should This Be in Domain?"
Ask these questions:
✅ Move to Domain If:
- Is it a pure function with no framework dependencies?
- Does it work with domain types?
- Could both frontend and backend use it?
- Is it business logic (not infrastructure)?
❌ Keep in Apps If:
- Does it import React, Next.js, or NestJS?
- Does it use environment variables?
- Does it depend on external libraries (API clients, HTTP libs)?
- Is it UI-specific or framework-specific?
- Is it only used in one app?
📋 Examples with Decisions
| Utility | Decision | Location | Why |
|---|---|---|---|
formatCurrency(amount, currency) |
✅ Domain | domain/toolkit/formatting/ |
Pure function, no deps |
invoiceQueryParamsSchema |
✅ Domain | domain/billing/schema.ts |
Domain-specific validation |
paginationParamsSchema |
✅ Domain | domain/common/schema.ts |
Truly generic |
useInvoices() React hook |
❌ Portal App | portal/lib/hooks/ |
React-specific |
apiClient instance |
❌ Portal App | portal/lib/api/ |
Framework-specific |
ZodValidationPipe |
✅ Validation Package | packages/validation/ |
Reusable bridge |
getErrorMessage(error) |
❌ BFF App | bff/core/utils/ |
App-specific utility |
transformWhmcsInvoice() |
✅ Domain | domain/billing/providers/whmcs/ |
Data transformation |
🚀 What About Query Parameters?
Current Structure (✅ Correct!)
Generic building blocks in domain/common/:
// packages/domain/common/schema.ts
export const paginationParamsSchema = z.object({
page: z.coerce.number().int().positive().optional(),
limit: z.coerce.number().int().positive().max(100).optional(),
});
export const filterParamsSchema = z.object({
search: z.string().optional(),
sortBy: z.string().optional(),
sortOrder: z.enum(["asc", "desc"]).optional(),
});
Domain-specific query params in their own domains:
// packages/domain/billing/schema.ts
export const invoiceQueryParamsSchema = z.object({
page: z.coerce.number().int().positive().optional(),
limit: z.coerce.number().int().positive().max(100).optional(),
status: invoiceStatusSchema.optional(), // ← Domain-specific
dateFrom: z.string().datetime().optional(), // ← Domain-specific
dateTo: z.string().datetime().optional(), // ← Domain-specific
});
// packages/domain/subscriptions/schema.ts
export const subscriptionQueryParamsSchema = z.object({
page: z.coerce.number().int().positive().optional(),
limit: z.coerce.number().int().positive().max(100).optional(),
status: subscriptionStatusSchema.optional(), // ← Domain-specific
type: z.string().optional(), // ← Domain-specific
});
Why this works:
commonhas truly generic utilities- Each domain owns its specific query parameters
- No duplication of business logic
📖 Summary
Domain Package (packages/domain/)
Contains:
- ✅ Domain types & interfaces
- ✅ Zod validation schemas
- ✅ Provider mappers (WHMCS, Salesforce, Freebit)
- ✅ Pure utility functions (formatting, parsing)
- ✅ Domain-specific query parameter schemas
Does NOT contain:
- ❌ React hooks
- ❌ API client instances
- ❌ Framework-specific code
- ❌ Infrastructure code
App Lib/Core Directories
Contains:
- ✅ Framework-specific utilities
- ✅ API clients & HTTP interceptors
- ✅ React hooks & custom hooks
- ✅ Error handling with framework dependencies
- ✅ Application-specific helpers
Key Takeaway: The domain package is your single source of truth for types and validation. Everything else stays in apps where it belongs!