Overview
Entity Evolution: Building a Robust Host Entity
This section demonstrates the step-by-step evolution of a Host entity from basic TypeScript patterns to a robust, type-safe implementation using functional programming principles.
BaseEntity provides id, createdAt, updatedAt and serialization helpers. This step defines IHost interface and Host class to describe the host entity structure.
Refined types ensure userId is a valid UUID and address is a structured value object. This step adds userId, dob, phoneNumber, address, socialLinks, and profileImage to the Host class.
Option<T> makes missing values explicit and type-safe. This step wraps dob, phoneNumber, and profileImage in Option to handle optional host data.
Constructors accept raw input and initialize BaseEntity fields via super(). This step adds an empty constructor that will be progressively filled with validation and assignment logic.
hasCompleteProfile checks if required profile fields are present for business rules. This step returns true when dob, phoneNumber, and profileImage all contain values.
SerializedHost represents the entity format for external systems and databases. This step defines a type with string id, nullable dates, and simplified value objects. Learn more: Type Boundaries
Maybe<T> accepts T | Option<T> | null | undefined from different data sources. This step replaces verbose unions with a single Maybe<T> type alias.
normalizeMaybe converts Maybe<T> to Option<T> at entity boundaries to keep domain logic uniform. This step handles null, undefined, existing Option, and raw values in one function.
serialize converts internal Option<T> values back to external Maybe<T> format. This step uses optionToMaybe to transform Host fields for APIs and databases.
optionToMaybe centralizes the Option<T> to Maybe<T> transformation logic. This step extracts the helper to reduce duplication across serialize methods.
Guards ensure field values meet business rules before entity creation. This step adds validatePhoneNumber and validateDateOfBirth as private static methods.
Constructor validation prevents invalid entities from being created. This step normalizes inputs then runs validatePhoneNumber and validateDateOfBirth before assignment.
Private constructors prevent unvalidated entity creation via new. This step adds Host.create factory method that always runs validation before instantiation.
Separate guard files enable reuse across domain and application layers. This step moves validatePhoneNumber and validateDateOfBirth into HostGuards class.
Shared utilities prevent duplication across multiple entities. This step moves Maybe<T>, normalizeMaybe, and optionToMaybe into domain/utils/utils.ts.
Factory methods compose normalizeMaybe with HostGuards validators and throw HostValidationError. This step replaces inline validation with external guard calls.
Manual serialization creates repetitive code, type gaps, and exception-based errors. This step lists the issues we will solve with Effect Schema.
Effect Schema derives types and validation from a single definition. This step shows PersonSchema example that automatically generates types, validation, and transformations.
HostSchema replaces manual types and validation with declarative field definitions. This step defines schema with StringToUUID, Optional fields, and composed value object schemas.
Schema.Type and Schema.Encoded automatically generate runtime and serialized types. This step eliminates manual HostType and SerializedHost definitions by deriving them from HostSchema.
Schema.pipe integrates validation directly into field definitions. This step adds HostGuards.ValidDateOfBirth, ValidPhoneNumber, and ValidProfileImagePath to schema fields.
Schema-derived types eliminate manual Option conversions in constructors. This step assigns HostType fields directly without normalizeMaybe or type casting.
Effect<Host, HostError, never> returns typed errors instead of throwing exceptions. This step composes S.decodeUnknown, guards, and value object creation in a single pipeline.
S.encode(HostSchema) serializes entities without manual field mapping. This step replaces the multi-line serialize method with a single Effect-returning call.
Code Organization
The domain layer is organized by business entities and utilities, following domain-driven design principles:
Core Utilities Explained
1. Base Entity (base.entity.ts)
Provides foundational entity functionality with Effect support:
BaseEntity class: Abstract base with id, createdAt, updatedAtIEntity interface: Core entity contractSerializedEntity: Type for persistence layer_fromSerialized(): Safe entity reconstruction from data_serialize(): Convert entity to plain object
2. Refined Types (refined.types.ts)
Branded types with Effect Schema integration:
UUID: Branded UUID type with validationDateTime: Branded DateTime with coercionEmail: Validated email addressesStringToUUID: Effect Schema transformer for UUIDDateTimeFromAny: Union transformer for multiple date formatsOptional(): Helper for optional fields with null handling
3. Validation Utils (validation.utils.ts)
Reusable validation building blocks:
createNotEmptyFilter(): Factory for non-empty string validationOptional(): Wrapper for nullable schema fields
4. Base Repository (base.repository.ts)
Abstract repository pattern with Effect:
BaseRepository<T>: Generic repository base classRepositoryEffect<T, E>: Type alias for Effect-based operationsCommon CRUD operations: insert, update, fetchById, etc.
5. Base Errors (base.errors.ts)
Domain error hierarchy:
DomainError: Base error classValidationError: For validation failuresNotFoundError: For missing entitiesAlreadyExistsError: For duplicate entities
6. Pagination (pagination.ts)
Effect-based pagination utilities:
PaginationOptions: Validated pagination parametersPaginated<T>: Interface for paginated resultsPaginator: Utility for paginating collections