314 lines
10 KiB
Markdown
314 lines
10 KiB
Markdown
# 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!
|
|
|