Documentation Index Fetch the complete documentation index at: https://woltz.mintlify.app/llms.txt
Use this file to discover all available pages before exploring further.
Overview
Mappers are responsible for transforming data between your domain model (Entities, Aggregates, Value Objects) and your persistence layer (database records). They keep your domain clean and independent of database concerns.
// Domain → Database
const record = userToPersistenceMapper . build ( user );
// Database → Domain
const user = userToDomainMapper . build ( record );
Why Use Mappers?
Separation of Concerns Domain models stay focused on business logic, not database schema
Schema Independence Change database schema without touching domain code
Data Transformation Handle type conversions, naming conventions, and structure differences
Testability Test domain logic without database dependencies
The Mapper Base Class
import { Mapper } from "@woltz/rich-domain" ;
abstract class Mapper < Input , Output > {
abstract build ( input : Input , ... args : unknown []) : Output ;
buildMany ( inputs : Input []) : Output [] {
return inputs . map (( input ) => this . build ( input ));
}
}
The base class is simple - implement build() to transform from Input to Output. Use buildMany() to transform arrays.
Creating Mappers
Domain to Persistence
Transform domain entities to database records:
interface UserRecord {
id : string ;
user_name : string ;
user_email : string ;
status : string ;
is_verified : boolean ;
created_at : Date ;
updated_at : Date ;
}
class UserToPersistenceMapper extends Mapper < User , UserRecord > {
build ( user : User ) : UserRecord {
return {
id: user . id . value , // Id → string
user_name: user . name , // camelCase → snake_case
user_email: user . email ,
status: user . status ,
is_verified: user . isVerified ,
created_at: new Date (),
updated_at: new Date (),
};
}
}
Persistence to Domain
Transform database records to domain entities:
class UserToDomainMapper extends Mapper < UserRecord , User > {
build ( record : UserRecord ) : User {
return new User ({
id: Id . from ( record . id ), // string → Id
name: record . user_name , // snake_case → camelCase
email: record . user_email ,
status: record . status as "active" | "inactive" ,
isVerified: record . is_verified ,
});
}
}
Handling Complex Structures
Nested Entities
interface OrderRecord {
id : string ;
customer_id : string ;
status : string ;
items : OrderItemRecord [];
}
interface OrderItemRecord {
id : string ;
order_id : string ;
product_id : string ;
quantity : number ;
unit_price : number ;
}
class OrderToDomainMapper extends Mapper < OrderRecord , Order > {
constructor ( private itemMapper : OrderItemToDomainMapper ) {
super ();
}
build ( record : OrderRecord ) : Order {
return new Order ({
id: Id . from ( record . id ),
customerId: record . customer_id ,
status: record . status as OrderStatus ,
items: record . items . map (( item ) => this . itemMapper . build ( item )),
});
}
}
class OrderItemToDomainMapper extends Mapper < OrderItemRecord , OrderItem > {
build ( record : OrderItemRecord ) : OrderItem {
return new OrderItem ({
id: Id . from ( record . id ),
productId: record . product_id ,
quantity: record . quantity ,
unitPrice: record . unit_price ,
});
}
}
Value Objects
interface UserRecord {
id : string ;
name : string ;
// Address is flattened in database
address_street : string | null ;
address_city : string | null ;
address_zip : string | null ;
}
class UserToDomainMapper extends Mapper < UserRecord , User > {
build ( record : UserRecord ) : User {
// Reconstruct Value Object from flat fields
const address = record . address_street
? new Address ({
street: record . address_street ,
city: record . address_city ! ,
zipCode: record . address_zip ! ,
})
: null ;
return new User ({
id: Id . from ( record . id ),
name: record . name ,
address ,
});
}
}
class UserToPersistenceMapper extends Mapper < User , UserRecord > {
build ( user : User ) : UserRecord {
return {
id: user . id . value ,
name: user . name ,
// Flatten Value Object
address_street: user . address ?. street ?? null ,
address_city: user . address ?. city ?? null ,
address_zip: user . address ?. zipCode ?? null ,
};
}
}
When nested entities are stored in separate tables:
// For reading - includes loaded relations
interface UserWithPostsRecord {
id : string ;
name : string ;
posts : PostRecord [];
}
class UserWithPostsToDomainMapper extends Mapper < UserWithPostsRecord , User > {
constructor ( private postMapper : PostToDomainMapper ) {
super ();
}
build ( record : UserWithPostsRecord ) : User {
return new User ({
id: Id . from ( record . id ),
name: record . name ,
posts: record . posts . map (( p ) => this . postMapper . build ( p )),
});
}
}
// For writing - only the user data (posts saved separately)
class UserToPersistenceMapper extends Mapper < User , UserRecord > {
build ( user : User ) : UserRecord {
return {
id: user . id . value ,
name: user . name ,
// Posts are NOT included - they're saved separately
};
}
}
Type Conversions
Common conversions between domain and persistence:
Id ↔ String
// Domain → Persistence
id : user . id . value // Id → string
// Persistence → Domain
id : Id . from ( record . id ) // string → Id
Enum ↔ String
// Domain
type OrderStatus = "draft" | "confirmed" | "shipped" | "delivered" ;
// Domain → Persistence
status : order . status // Already a string
// Persistence → Domain
status : record . status as OrderStatus // Cast to union type
Date Handling
// Domain → Persistence
createdAt : user . createdAt // Date stays as Date for most ORMs
// Persistence → Domain
createdAt : new Date ( record . created_at ) // If stored as string/timestamp
JSON Fields
// Domain
interface UserSettings {
theme : "light" | "dark" ;
notifications : boolean ;
}
// Domain → Persistence (JSON column)
settings : JSON . stringify ( user . settings )
// Persistence → Domain
settings : JSON . parse ( record . settings ) as UserSettings
Nullable Relations
// Domain → Persistence
addressId : user . address ?. id . value ?? null
// Persistence → Domain (load address in repository query, not in mapper)
address : record . address
? new Address ({ street: record . address . street , city: record . address . city })
: null
Mapper Composition
Compose mappers for complex aggregates:
class AggregateMappers {
constructor (
readonly userToDomain : UserToDomainMapper ,
readonly userToPersistence : UserToPersistenceMapper ,
readonly postToDomain : PostToDomainMapper ,
readonly postToPersistence : PostToPersistenceMapper ,
readonly commentToDomain : CommentToDomainMapper ,
readonly commentToPersistence : CommentToPersistenceMapper
) {}
}
// Create all mappers together
const mappers = new AggregateMappers (
new UserToDomainMapper (),
new UserToPersistenceMapper (),
new PostToDomainMapper (),
new PostToPersistenceMapper (),
new CommentToDomainMapper (),
new CommentToPersistenceMapper ()
);
// Use in repository
class UserRepository extends Repository < User > {
constructor (
private db : Database ,
private mappers : AggregateMappers
) {
super ();
}
protected get mapperToDomain () {
return this . mappers . userToDomain ;
}
protected get mapperToPersistence () {
return this . mappers . userToPersistence ;
}
}
Mapping Lists
The base Mapper class includes a buildMany() method for transforming arrays:
class UserToDomainMapper extends Mapper < UserRecord , User > {
build ( record : UserRecord ) : User {
return new User ({
id: Id . from ( record . id ),
name: record . name ,
});
}
}
// Usage - buildMany is inherited from Mapper
const users = userMapper . buildMany ( records );
Using with Repository
class UserRepository extends Repository < User > {
constructor (
private db : PrismaClient ,
protected readonly mapperToDomain : UserToDomainMapper ,
protected readonly mapperToPersistence : UserToPersistenceMapper
) {
super ();
}
async findById ( id : string ) : Promise < User | null > {
const record = await this . db . user . findUnique ({
where: { id },
include: { posts: true },
});
return record ? this . mapperToDomain . build ( record ) : null ;
}
async save ( user : User ) : Promise < void > {
const data = this . mapperToPersistence . build ( user );
if ( user . isNew ()) {
await this . db . user . create ({ data });
} else {
await this . db . user . update ({
where: { id: user . id . value },
data ,
});
}
}
}
Testing Mappers
describe ( "UserToDomainMapper" , () => {
const mapper = new UserToDomainMapper ();
it ( "should map record to domain" , () => {
const record : UserRecord = {
id: "user-123" ,
user_name: "John Doe" ,
user_email: "john@example.com" ,
status: "active" ,
is_verified: true ,
created_at: new Date (),
updated_at: new Date (),
};
const user = mapper . build ( record );
expect ( user ). toBeInstanceOf ( User );
expect ( user . id . value ). toBe ( "user-123" );
expect ( user . name ). toBe ( "John Doe" );
expect ( user . email ). toBe ( "john@example.com" );
expect ( user . status ). toBe ( "active" );
expect ( user . isVerified ). toBe ( true );
});
it ( "should handle null address" , () => {
const record : UserRecord = {
id: "user-123" ,
user_name: "John" ,
address_street: null ,
address_city: null ,
address_zip: null ,
};
const user = mapper . build ( record );
expect ( user . address ). toBeNull ();
});
});
describe ( "UserToPersistenceMapper" , () => {
const mapper = new UserToPersistenceMapper ();
it ( "should map domain to record" , () => {
const user = new User ({
id: Id . from ( "user-123" ),
name: "John Doe" ,
email: "john@example.com" ,
status: "active" ,
isVerified: true ,
});
const record = mapper . build ( user );
expect ( record . id ). toBe ( "user-123" );
expect ( record . user_name ). toBe ( "John Doe" );
expect ( record . user_email ). toBe ( "john@example.com" );
});
});