- Removed the domain mappings module, consolidating related types and schemas into the id-mappings feature. - Updated import paths across the BFF to reflect the new structure, ensuring compliance with import hygiene rules. - Cleaned up unused files and optimized the codebase for better maintainability and clarity.
@customer-portal/domain
Single Source of Truth for Types and Validation
The @customer-portal/domain package is the centralized domain layer containing all types, Zod validation schemas, and provider-specific adapters for the customer portal application.
📦 Package Structure
packages/domain/
├── common/ # Shared types and utilities
│ ├── types.ts # Common types, API responses, pagination
│ ├── schema.ts # Zod schemas for validation
│ └── index.ts
│
├── auth/ # Authentication & authorization
│ ├── contract.ts # User, AuthTokens, AuthResponse types
│ ├── schema.ts # Login, signup, password validation
│ └── index.ts
│
├── billing/ # Invoices and billing
│ ├── contract.ts # Invoice, InvoiceItem, InvoiceList
│ ├── schema.ts # Zod schemas + query params
│ ├── providers/whmcs/ # WHMCS adapter
│ └── index.ts
│
├── subscriptions/ # Service subscriptions
│ ├── contract.ts # Subscription, SubscriptionStatus
│ ├── schema.ts # Zod schemas + query params
│ ├── providers/whmcs/ # WHMCS adapter
│ └── index.ts
│
├── orders/ # Order management
│ ├── contract.ts # OrderSummary, OrderDetails
│ ├── schema.ts # Zod schemas + query params
│ ├── providers/
│ │ ├── salesforce/ # Read orders from Salesforce
│ │ └── whmcs/ # Create orders in WHMCS
│ └── index.ts
│
├── payments/ # Payment methods & gateways
│ ├── contract.ts # PaymentMethod, PaymentGateway
│ ├── schema.ts # Zod validation schemas
│ └── index.ts
│
├── sim/ # SIM card management
│ ├── contract.ts # SimDetails, SimUsage
│ ├── schema.ts # Zod schemas + activation
│ ├── providers/freebit/ # Freebit adapter
│ └── index.ts
│
├── catalog/ # Product catalog
│ ├── contract.ts # CatalogProduct types
│ ├── schema.ts # Product validation
│ └── index.ts
│
├── customer/ # Customer data
│ ├── contract.ts # Customer, CustomerAddress
│ ├── schema.ts # Customer validation
│ └── index.ts
│
└── toolkit/ # Utilities
├── formatting/ # Currency, date formatting
├── validation/ # Validation helpers
└── index.ts
🎯 Design Principles
1. Domain-First Organization
Each business domain owns its:
contract.ts- TypeScript interfaces (provider-agnostic)schema.ts- Zod validation schemas (runtime safety)providers/- Provider-specific adapters (WHMCS, Salesforce, Freebit)
2. Single Source of Truth
- ✅ All types defined in domain package
- ✅ All validation schemas in domain package
- ✅ No duplicate type definitions in apps
- ✅ Shared between frontend (Next.js) and backend (NestJS)
3. Type Safety + Runtime Validation
- TypeScript provides compile-time type checking
- Zod schemas provide runtime validation
- Use
z.infer<typeof schema>to derive types from schemas
📚 Usage Guide
Basic Import Pattern
// Import domain types and schemas
import { Invoice, invoiceSchema, InvoiceQueryParams } from "@customer-portal/domain/billing";
import { Subscription, subscriptionSchema } from "@customer-portal/domain/subscriptions";
import { ApiResponse, PaginationParams } from "@customer-portal/domain/common";
API Response Handling
import {
ApiResponse,
ApiSuccessResponse,
ApiErrorResponse,
apiResponseSchema,
} from "@customer-portal/domain/common";
// Type-safe API responses
const response: ApiResponse<Invoice> = {
success: true,
data: {
/* invoice data */
},
};
// With validation
const validated = apiResponseSchema(invoiceSchema).parse(rawResponse);
Query Parameters with Validation
import {
InvoiceQueryParams,
invoiceQueryParamsSchema
} from "@customer-portal/domain/billing";
// In BFF controller
@Get()
@UsePipes(new ZodValidationPipe(invoiceQueryParamsSchema))
async getInvoices(@Query() query: InvoiceQueryParams) {
// query is validated and typed
}
// In frontend
const params: InvoiceQueryParams = {
page: 1,
limit: 20,
status: "Unpaid"
};
Form Validation (Frontend)
import { useZodForm } from "@customer-portal/validation";
import { loginRequestSchema, type LoginRequest } from "@customer-portal/domain/auth";
function LoginForm() {
const form = useZodForm<LoginRequest>({
schema: loginRequestSchema,
initialValues: { email: "", password: "" },
onSubmit: async (data) => {
// data is validated and typed
await login(data);
},
});
return (
<form onSubmit={form.handleSubmit}>
{/* form fields */}
</form>
);
}
Backend Validation (BFF)
import { ZodValidationPipe } from "@bff/core/validation";
import { createOrderRequestSchema, type CreateOrderRequest } from "@customer-portal/domain/orders";
@Controller("orders")
export class OrdersController {
@Post()
@UsePipes(new ZodValidationPipe(createOrderRequestSchema))
async create(@Body() body: CreateOrderRequest) {
// body is validated by Zod before reaching here
return this.orderService.create(body);
}
}
🔧 Common Schemas Reference
API Responses
| Schema | Description |
|---|---|
apiSuccessResponseSchema(dataSchema) |
Successful API response wrapper |
apiErrorResponseSchema |
Error API response with code/message |
apiResponseSchema(dataSchema) |
Discriminated union of success/error |
Pagination & Queries
| Schema | Description |
|---|---|
paginationParamsSchema |
Page, limit, offset parameters |
paginatedResponseSchema(itemSchema) |
Paginated list response |
filterParamsSchema |
Search, sortBy, sortOrder |
queryParamsSchema |
Combined pagination + filters |
Domain-Specific Query Params
| Schema | Description |
|---|---|
invoiceQueryParamsSchema |
Invoice list filtering (status, dates) |
subscriptionQueryParamsSchema |
Subscription filtering (status, type) |
orderQueryParamsSchema |
Order filtering (status, orderType) |
Validation Primitives
| Schema | Description |
|---|---|
emailSchema |
Email validation (lowercase, trimmed) |
passwordSchema |
Strong password (8+ chars, mixed case, number, special) |
nameSchema |
Name validation (1-100 chars) |
phoneSchema |
Phone number validation |
timestampSchema |
ISO datetime string |
dateSchema |
ISO date string |
🚀 Adding New Domain Types
1. Create Domain Files
// packages/domain/my-domain/contract.ts
export interface MyEntity {
id: string;
name: string;
status: "active" | "inactive";
}
export interface MyEntityList {
entities: MyEntity[];
totalCount: number;
}
// packages/domain/my-domain/schema.ts
import { z } from "zod";
export const myEntityStatusSchema = z.enum(["active", "inactive"]);
export const myEntitySchema = z.object({
id: z.string(),
name: z.string().min(1),
status: myEntityStatusSchema,
});
export const myEntityListSchema = z.object({
entities: z.array(myEntitySchema),
totalCount: z.number().int().nonnegative(),
});
// Query params
export const myEntityQueryParamsSchema = z.object({
page: z.coerce.number().int().positive().optional(),
limit: z.coerce.number().int().positive().max(100).optional(),
status: myEntityStatusSchema.optional(),
});
export type MyEntityQueryParams = z.infer<typeof myEntityQueryParamsSchema>;
// packages/domain/my-domain/index.ts
export * from "./contract";
export * from "./schema";
2. Use in Backend (BFF)
import { ZodValidationPipe } from "@bff/core/validation";
import {
myEntitySchema,
myEntityQueryParamsSchema,
type MyEntity,
type MyEntityQueryParams,
} from "@customer-portal/domain/my-domain";
@Controller("my-entities")
export class MyEntitiesController {
@Get()
@UsePipes(new ZodValidationPipe(myEntityQueryParamsSchema))
async list(@Query() query: MyEntityQueryParams): Promise<MyEntityList> {
return this.service.list(query);
}
}
3. Use in Frontend
import { useQuery } from "@tanstack/react-query";
import { myEntitySchema, type MyEntity } from "@customer-portal/domain/my-domain";
function useMyEntities() {
return useQuery({
queryKey: ["my-entities"],
queryFn: async () => {
const response = await apiClient.get("/my-entities");
return myEntitySchema.array().parse(response.data);
},
});
}
✅ Validation Best Practices
1. Always Define Both Type and Schema
// ✅ Good - Type and schema together
export const userSchema = z.object({
id: z.string(),
email: emailSchema,
});
export type User = z.infer<typeof userSchema>;
// ❌ Bad - Type only (no runtime validation)
export interface User {
id: string;
email: string;
}
2. Use Zod Schema Composition
// Base schema
const baseProductSchema = z.object({
id: z.string(),
name: z.string(),
});
// Extended schema
export const fullProductSchema = baseProductSchema.extend({
description: z.string(),
price: z.number().positive(),
});
3. Query Params Use z.coerce for URL Strings
// ✅ Good - coerce string params to numbers
export const paginationSchema = z.object({
page: z.coerce.number().int().positive().optional(),
limit: z.coerce.number().int().positive().optional(),
});
// ❌ Bad - will fail on URL query strings
export const paginationSchema = z.object({
page: z.number().int().positive().optional(), // "1" !== 1
});
4. Use Refinements for Complex Validation
export const simActivationSchema = z
.object({
simType: z.enum(["eSIM", "Physical SIM"]),
eid: z.string().optional(),
})
.refine(data => data.simType !== "eSIM" || (data.eid && data.eid.length >= 15), {
message: "EID required for eSIM",
path: ["eid"],
});
🔄 Migration from Local Types
If you find types defined locally in apps, migrate them to domain:
// ❌ Before: apps/bff/src/modules/invoices/types.ts
export interface InvoiceQuery {
status?: string;
page?: number;
}
// ✅ After: packages/domain/billing/schema.ts
export const invoiceQueryParamsSchema = z.object({
status: invoiceStatusSchema.optional(),
page: z.coerce.number().int().positive().optional(),
});
export type InvoiceQueryParams = z.infer<typeof invoiceQueryParamsSchema>;
// Update imports
import { InvoiceQueryParams } from "@customer-portal/domain/billing";
📖 Additional Resources
- Zod Documentation: https://zod.dev/
- Provider-Aware Architecture: See
docs/DOMAIN-STRUCTURE.md - Type System: See
docs/CONSOLIDATED-TYPE-SYSTEM.md
🤝 Contributing
When adding new types or schemas:
- ✅ Define types in
contract.ts - ✅ Add Zod schemas in
schema.ts - ✅ Export from
index.ts - ✅ Update this README if adding new patterns
- ✅ Remove any duplicate types from apps
- ✅ Update imports to use domain package
Maintained by: Customer Portal Team
Last Updated: October 2025