Your application lives in three worlds: API contracts (what clients expect), domain models (your business logic), and database schemas (how data persists). Each world has different requirements. The API uses snake_case timestamps in milliseconds; your domain uses camelCase Dates; the database stores ISO strings. Converting between these three representations is error-prone. You need a unified system that handles both directions—decode from external sources, encode for external consumption.
import { Schema, Effect } from "effect"
// ============================================
// 1. Domain Layer (source of truth)
// ============================================
type DomainUser = {
userId: string
email: string
fullName: string
createdAt: Date
updatedAt: Date
isActive: boolean
}
// ============================================
// 2. API Contract (client-facing)
// ============================================
type ApiUserRequest = {
email: string
full_name: string
}
type ApiUserResponse = {
user_id: string
email: string
full_name: string
created_at: number // milliseconds
updated_at: number // milliseconds
is_active: boolean
}
// ============================================
// 3. Database Schema (persistence layer)
// ============================================
type DbUserRow = {
user_id: string
email: string
full_name: string
created_at: string // ISO 8601
updated_at: string // ISO 8601
is_active: boolean
}
// ============================================
// 4. Define transformations: API → Domain
// ============================================
const ApiUserResponseSchema = Schema.Struct({
user_id: Schema.String,
email: Schema.String.pipe(
Schema.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)
),
full_name: Schema.String,
created_at: Schema.transform(Schema.Number, Schema.Date, {
decode: (ms) => new Date(ms),
encode: (date) => date.getTime(),
}),
updated_at: Schema.transform(Schema.Number, Schema.Date, {
decode: (ms) => new Date(ms),
encode: (date) => date.getTime(),
}),
is_active: Schema.Boolean,
}).pipe(
Schema.transform(
Schema.Struct({
user_id: Schema.String,
email: Schema.String,
full_name: Schema.String,
created_at: Schema.Date,
updated_at: Schema.Date,
is_active: Schema.Boolean,
}),
Schema.Struct({
userId: Schema.String,
email: Schema.String,
fullName: Schema.String,
createdAt: Schema.Date,
updatedAt: Schema.Date,
isActive: Schema.Boolean,
}),
{
decode: (api) => ({
userId: api.user_id,
email: api.email,
fullName: api.full_name,
createdAt: api.created_at,
updatedAt: api.updated_at,
isActive: api.is_active,
}),
encode: (domain) => ({
user_id: domain.userId,
email: domain.email,
full_name: domain.fullName,
created_at: domain.createdAt,
updated_at: domain.updatedAt,
is_active: domain.isActive,
}),
}
)
)
// ============================================
// 5. Define transformations: DB → Domain
// ============================================
const DbUserRowSchema = Schema.Struct({
user_id: Schema.String,
email: Schema.String,
full_name: Schema.String,
created_at: Schema.transform(Schema.String, Schema.Date, {
decode: (isoString) => new Date(isoString),
encode: (date) => date.toISOString(),
}),
updated_at: Schema.transform(Schema.String, Schema.Date, {
decode: (isoString) => new Date(isoString),
encode: (date) => date.toISOString(),
}),
is_active: Schema.Boolean,
}).pipe(
Schema.transform(
Schema.Struct({
user_id: Schema.String,
email: Schema.String,
full_name: Schema.String,
created_at: Schema.Date,
updated_at: Schema.Date,
is_active: Schema.Boolean,
}),
Schema.Struct({
userId: Schema.String,
email: Schema.String,
fullName: Schema.String,
createdAt: Schema.Date,
updatedAt: Schema.Date,
isActive: Schema.Boolean,
}),
{
decode: (db) => ({
userId: db.user_id,
email: db.email,
fullName: db.full_name,
createdAt: db.created_at,
updatedAt: db.updated_at,
isActive: db.is_active,
}),
encode: (domain) => ({
user_id: domain.userId,
email: domain.email,
full_name: domain.fullName,
created_at: domain.createdAt,
updated_at: domain.updatedAt,
is_active: domain.isActive,
}),
}
)
)
// ============================================
// 6. Create repositories with transformations
// ============================================
class UserRepository {
// Decode: DB row → Domain model
async findById(id: string): Promise<DomainUser | null> {
// Simulate database query
const dbRow: DbUserRow = {
user_id: "user_123",
email: "alice@example.com",
full_name: "Alice Smith",
created_at: "2024-01-15T10:30:00Z",
updated_at: "2024-12-17T14:22:00Z",
is_active: true,
}
const decoded = await Schema.decodeUnknown(DbUserRowSchema)(dbRow)
return decoded
}
// Encode: Domain model → DB row
async save(user: DomainUser): Promise<void> {
const dbRow = await Schema.encode(DbUserRowSchema)(user)
console.log("📦 Saving to database:", dbRow)
}
}
// ============================================
// 7. Create API handlers with transformations
// ============================================
class UserApiHandler {
private repo = new UserRepository()
// Receive API response → Domain → Store
async handleIncomingUser(apiData: unknown): Promise<DomainUser> {
const domainUser = await Schema.decodeUnknown(ApiUserResponseSchema)(apiData)
await this.repo.save(domainUser)
return domainUser
}
// Retrieve Domain → API response
async getUserById(id: string): Promise<ApiUserResponse> {
const domainUser = await this.repo.findById(id)
if (!domainUser) {
throw new Error("User not found")
}
const apiResponse = await Schema.encode(ApiUserResponseSchema)(domainUser)
return apiResponse
}
}
// ============================================
// 8. Application logic
// ============================================
const appLogic = Effect.gen(function* () {
const handler = new UserApiHandler()
// Simulate incoming API response from external service
const incomingApiData: ApiUserResponse = {
user_id: "user_123",
email: "alice@example.com",
full_name: "Alice Smith",
created_at: 1705317000000, // milliseconds
updated_at: 1734443520000, // milliseconds
is_active: true,
}
console.log("📥 Incoming API data:", incomingApiData)
// Decode: API → Domain → Save to DB
const domainUser = yield* Effect.tryPromise({
try: () => handler.handleIncomingUser(incomingApiData),
catch: (error) => {
const msg = error instanceof Error ? error.message : String(error)
return new Error(`Failed to process user: ${msg}`)
},
})
console.log("\n✅ Domain model:", {
userId: domainUser.userId,
email: domainUser.email,
fullName: domainUser.fullName,
createdAt: domainUser.createdAt.toISOString(),
updatedAt: domainUser.updatedAt.toISOString(),
})
// Encode: Domain → API response for client
const apiResponse = yield* Effect.tryPromise({
try: () => handler.getUserById("user_123"),
catch: (error) => {
const msg = error instanceof Error ? error.message : String(error)
return new Error(`Failed to retrieve user: ${msg}`)
},
})
console.log("\n📤 Outgoing API response:", apiResponse)
return { domainUser, apiResponse }
})
// Run application
Effect.runPromise(appLogic)
.then(() => console.log("\n✅ Transformation complete"))
.catch((error) => console.error(`Error: ${error.message}`))