# 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 ```typescript // packages/domain/billing/contract.ts export interface Invoice { id: number; status: InvoiceStatus; total: number; } ``` #### 2. Validation Schemas (Zod) ```typescript // 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 ```typescript // 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 ```typescript // 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 ```typescript // ❌ 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 ```typescript // ❌ 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 ```typescript // ❌ 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 ```typescript // ❌ 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: 1. Is it a **pure function** with no framework dependencies? 2. Does it work with **domain types**? 3. Could **both frontend and backend** use it? 4. Is it **business logic** (not infrastructure)? ### ❌ Keep in Apps If: 1. Does it import **React**, **Next.js**, or **NestJS**? 2. Does it use **environment variables**? 3. Does it depend on **external libraries** (API clients, HTTP libs)? 4. Is it **UI-specific** or **framework-specific**? 5. 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/`: ```typescript // 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: ```typescript // 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:** - `common` has 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!