--- url: /examples/blog.md --- # Blog Example: Posts and Authors This example demonstrates how to set up domain models, view models, and mappers for a blog application with nested mapping (a `Post` contains an `Author`). ## 1. Define Domain Models ```ts [domain/models/AuthorDomainModel.ts] export interface AuthorDomainModel { id: string; name: string; avatar_url: string; } ``` ```ts [domain/models/PostDomainModel.ts] import type { AuthorDomainModel } from './AuthorDomainModel'; export interface PostDomainModel { id: string; title: string; content: string; author: AuthorDomainModel; published_at: string; } ``` ## 2. Define View Models ```ts [view/models/AuthorViewModel.ts] export interface AuthorViewModel { id: string; displayName: string; avatarUrl: string; } ``` ```ts [view/models/PostViewModel.ts] import type { AuthorViewModel } from './AuthorViewModel'; export interface PostViewModel { id: string; title: string; content: string; author: AuthorViewModel; publishedAt: Date; } ``` ## 3. Define Mappers (Nested Mapping) ```ts [view/mappers/AuthorMapper.ts] import type { AuthorDomainModel } from '@/data/domain/models'; import type { AuthorViewModel } from '../models'; import type { ModelMapper } from './base'; export class AuthorMapper implements ModelMapper { toViewModel(domain: AuthorDomainModel): AuthorViewModel { return { id: domain.id, displayName: domain.name, avatarUrl: domain.avatar_url, }; } toDomainModel(view: AuthorViewModel): AuthorDomainModel { return { id: view.id, name: view.displayName, avatar_url: view.avatarUrl, }; } } ``` ```ts [view/mappers/PostMapper.ts] import type { PostDomainModel } from '@/data/domain/models'; import type { PostViewModel } from '../models'; import { AuthorMapper } from './AuthorMapper'; import type { ModelMapper } from './base'; const authorMapper = new AuthorMapper(); export class PostMapper implements ModelMapper { toViewModel(domain: PostDomainModel): PostViewModel { return { id: domain.id, title: domain.title, content: domain.content, author: authorMapper.toViewModel(domain.author), publishedAt: new Date(domain.published_at), }; } toDomainModel(view: PostViewModel): PostDomainModel { return { id: view.id, title: view.title, content: view.content, author: authorMapper.toDomainModel(view.author), published_at: view.publishedAt.toISOString(), }; } } ``` ## 4. Usage Example ```ts [usage-example.ts] import { PostMapper } from '@/data/view/mappers/PostMapper'; const postMapper = new PostMapper(); const domainPost = { id: '1', title: 'Hello World', content: 'Welcome to the blog!', author: { id: 'a1', name: 'Alice', avatar_url: '/alice.png' }, published_at: '2025-06-01T10:00:00Z', }; const viewPost = postMapper.toViewModel(domainPost); // viewPost.author.displayName === 'Alice' ``` *** This pattern can be extended for more complex nested models (e.g., comments with authors, etc.). --- --- url: /examples/ecommerce.md --- # E-Commerce Example: Products and Categories This example shows how to model an e-commerce product with a nested category, and how to set up mappers for both. ## 1. Define Domain Models ```ts [domain/models/CategoryDomainModel.ts] export interface CategoryDomainModel { id: string; name: string; } ``` ```ts [domain/models/ProductDomainModel.ts] import type { CategoryDomainModel } from './CategoryDomainModel'; export interface ProductDomainModel { id: string; name: string; price_cents: number; category: CategoryDomainModel; image_url: string; } ``` ## 2. Define View Models ```ts [view/models/CategoryViewModel.ts] export interface CategoryViewModel { id: string; label: string; } ``` ```ts [view/models/ProductViewModel.ts] import type { CategoryViewModel } from './CategoryViewModel'; export interface ProductViewModel { id: string; name: string; price: string; // formatted, e.g. "$19.99" category: CategoryViewModel; imageUrl: string; } ``` ## 3. Define Mappers (Nested Mapping) ```ts [view/mappers/CategoryMapper.ts] import type { CategoryDomainModel } from '@/data/domain/models'; import type { CategoryViewModel } from '../models'; import type { ModelMapper } from './base'; export class CategoryMapper implements ModelMapper { toViewModel(domain: CategoryDomainModel): CategoryViewModel { return { id: domain.id, label: domain.name, }; } toDomainModel(view: CategoryViewModel): CategoryDomainModel { return { id: view.id, name: view.label, }; } } ``` ```ts [view/mappers/ProductMapper.ts] import type { ProductDomainModel } from '@/data/domain/models'; import type { ProductViewModel } from '../models'; import { CategoryMapper } from './CategoryMapper'; import type { ModelMapper } from './base'; const categoryMapper = new CategoryMapper(); export class ProductMapper implements ModelMapper { toViewModel(domain: ProductDomainModel): ProductViewModel { return { id: domain.id, name: domain.name, price: `$${(domain.price_cents / 100).toFixed(2)}`, category: categoryMapper.toViewModel(domain.category), imageUrl: domain.image_url, }; } toDomainModel(view: ProductViewModel): ProductDomainModel { return { id: view.id, name: view.name, price_cents: Math.round(parseFloat(view.price.replace('$', '')) * 100), category: categoryMapper.toDomainModel(view.category), image_url: view.imageUrl, }; } } ``` ## 4. Usage Example ```ts [usage-example.ts] import { ProductMapper } from '@/data/view/mappers/ProductMapper'; const productMapper = new ProductMapper(); const domainProduct = { id: 'p1', name: 'T-Shirt', price_cents: 1999, category: { id: 'c1', name: 'Apparel' }, image_url: '/tshirt.png', }; const viewProduct = productMapper.toViewModel(domainProduct); // viewProduct.price === "$19.99" // viewProduct.category.label === 'Apparel' ``` *** This approach can be extended to include more nested models, such as product variants, reviews, etc. --- --- url: /examples.md --- # Examples This section provides examples of how to use the Domain View Model Mapper (DVMM) in various scenarios. Each example demonstrates the structure and usage of mappers for converting between domain models and view models. * [Todo List](./todo-list.md) * [User Management](./user-management.md) * [Blog](./blog.md) * [E-Commerce](./ecommerce.md) * [Messaging](./messaging.md) * [Project Management](./project-management.md) --- --- url: /getting-started.md --- # Getting Started ## Introduction DVMM (Domain-View Model Mapper) is a frontend architecture pattern for TypeScript projects that enforces a clean separation between backend data models (domain models) and UI models (view models) using deterministic mappers. This approach improves maintainability, enables parallel backend/frontend development, and ensures type safety throughout your codebase. ## Folder structure A typical DVMM project is organized as follows: ``` src ├── data │ ├── domain │ │ ├── models # Domain models (backend API response structure) │ │ ├── functions # Functions to interact with backend APIs │ │ ├── client.ts # API client setup (e.g., Axios, Fetch, Ky) │ │ └── index.ts # Entry point for domain logic │ ├── view │ │ ├── models # View models (data structure needed by the UI) │ │ ├── mappers # Mappers to convert between domain and view models │ │ └── index.ts # Entry point for view logic ├── components └── pages ``` ### Folder Structure Explanation * **data/domain/models**: Contains TypeScript interfaces/types that mirror backend API responses. These should match the backend contract exactly. * **data/domain/functions**: Functions for fetching or mutating data via the backend API. * **data/domain/client.ts**: Sets up the API client (e.g., Axios instance or Fetch wrapper). * **data/domain/index.ts**: Aggregates and exports domain logic for easy imports. * **data/view/models**: Contains view model interfaces/types, shaped for UI needs (e.g., formatting, grouping, or flattening data). * **data/view/mappers**: Contains pure functions or classes that convert between domain and view models. * **data/view/index.ts**: Aggregates and exports view logic. * **components**: UI components that consume view models. * **pages**: Top-level pages or routes. ## Naming Conventions When creating your models and mappers, follow these naming conventions to ensure clarity and consistency across your codebase: | Concept | Naming Pattern | Example | | ------------ | -------------- | ----------------- | | Domain Model | `XDomainModel` | `UserDomainModel` | | View Model | `XViewModel` | `UserViewModel` | | Mapper | `XMapper` | `UserMapper` | ### ModelMapper interface To ensure consistency in your mappers, define a common interface for them. This interface should include methods for converting between domain and view models. ```ts export interface ModelMapper { toViewModel(domainModel: DomainModel): ViewModel; toDomainModel(viewModel: ViewModel): DomainModel; } ``` ## Example Implementation Let's take an example of a `Post` entity with typical read endpoints. In a RESTful API, these endpoints might look like: | Endpoint | Method | Description | | ------------- | ------ | ------------------------ | | `/posts` | GET | Retrieve a list of posts | These endpoints allow you to fetch either a collection of posts or the details of a specific post. We'll use this `Post` entity to demonstrate how DVMM structures domain and view models, as well as the mapping logic between them. **Define the domain model for Post entity** To get started, you'll first define the domain model, which should match the backend API's response structure exactly. Create a file named `PostDomainModel.ts` in the `data/domain/models` directory: ::: code-group ```ts [data/domain/models/PostDomainModel.ts] export interface PostDomainModel { id: string; title: string; description: string; stars: number; created_at: string; // ISO date string updated_at: string; // ISO date string } ``` ```ts [data/domain/models/index.ts] export { PostDomainModel } from './PostDomainModel'; // [!code ++] ``` ::: **Define the functions for api access** Next, create a file named `posts.ts` in the `data/domain/functions` directory. This file will contain functions to interact with the backend API. ::: code-group ```ts [data/domain/functions/posts.ts] import { PostDomainModel } from '../models/post'; import type { KyInstance } from 'ky'; // -- the ApiClient can be an Axios instance, Fetch wrapper, or any HTTP client you prefer // for this example, we'll use a ky instance // the client should be configured to point to your backend API base URL export async function fetchPosts(client: KyInstance): Promise { return await client.get('posts').json(); } ``` ```ts [data/domain/functions/index.ts] export * as posts from './posts'; // [!code ++] ``` ::: We can further organize our code by creating an `index.ts` file inside the `data/domain` directory to aggregate and export all domain logic: ```ts [data/domain/index.ts] export * as models from './models'; // [!code ++] export * as fn from './functions'; // [!code ++] ``` **Define the view model for Post entity** Now, create the view model that will be used by the UI. This model may include additional formatting or derived properties that are not present in the domain model. Create a file named `PostViewModel.ts` in the `data/view/models` directory: ::: code-group ```ts [data/view/models/PostViewModel.ts] export interface PostViewModel { id: string; title: string; description: string; stars: number; createdAt: Date; // Converted to Date object for easier manipulation in UI updatedAt: Date; // Converted to Date object for easier manipulation in UI } ``` ```ts [data/view/models/index.ts] export { PostViewModel } from './PostViewModel'; // [!code ++] ``` ::: **Define the mapper for Post entity** Next, create a mapper that converts between the domain model and the view model. This mapper will handle any necessary transformations, such as converting date strings to Date objects. Create a file named `PostMapper.ts` in the `data/view/mappers` directory: ::: code-group ```ts [data/view/mappers/PostMapper.ts] import type { PostDomainModel } from '@/data/domain/models'; import type { PostViewModel } from '../models'; import type { ModelMapper } from './base'; export class PostMapper implements ModelMapper { toViewModel(domain: PostDomainModel): PostViewModel { return { id: domain.id, title: domain.title, description: domain.description, stars: domain.stars, createdAt: new Date(domain.created_at), updatedAt: new Date(domain.updated_at), }; } toDomainModel(view: PostViewModel): PostDomainModel { return { id: view.id, title: view.title, description: view.description, stars: view.stars, created_at: view.createdAt.toISOString(), updated_at: view.updatedAt.toISOString(), }; } } ``` ```ts [data/view/mappers/base.ts] export interface ModelMapper { toViewModel(domainModel: DomainModel): ViewModel; toDomainModel(viewModel: ViewModel): DomainModel; } ``` ```ts [data/view/mappers/index.ts] export { PostMapper } from './PostMapper'; // [!code ++] ``` **Using the Mapper** Here's how you can use `PostMapper` with React Query to fetch and map posts for your UI: ::: code-group ```ts [src/hooks/use-posts.ts] import { useQuery } from '@tanstack/react-query'; import { fn } from '@/data/domain'; import { PostMapper } from '@/data/view/mappers'; import { useApiClient } from '@/data/domain/client'; const postMapper = new PostMapper(); export function usePosts() { const client = useApiClient(); // Assume this is a hook that provides your API client return useQuery({ queryKey: ['posts'], queryFn: async () => { const domainPosts = await fn.posts.fetchPosts(client); return domainPosts.map(post => postMapper.toViewModel(post)); }, }); } ``` ```tsx [components/PostList.tsx] import { usePosts } from '@/hooks/use-posts'; import { PostItem } from './PostItem'; function PostList() { const { data: posts, isLoading, error } = usePosts(); if (isLoading) return
Loading...
; if (error) return
Error loading posts
; return (
{posts?.map(post => ( ))}
); } ``` ```tsx [components/PostItem.tsx] import { Card, CardHeader, CardTitle, CardContent, CardFooter } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import type { PostViewModel } from "@/data/view/models"; type PostItemProps = { post: PostViewModel; }; export function PostItem({ post }: PostItemProps) { return ( {post.title}

{post.description}

⭐ {post.stars}
{post.updatedAt.toLocaleDateString()}
); } ``` ::: ## Mapper Testing Example To ensure your mappers work correctly, you can write unit tests for them. Here's an example of how to test the `PostMapper`: ```ts [tests/PostMapper.test.ts] import { PostMapper } from '@/data/view/mappers'; import { PostDomainModel } from '@/data/domain/models'; const postMapper = new PostMapper(); const domainPost: PostDomainModel = { id: '1', title: 'Test Post', description: 'This is a test post.', stars: 5, created_at: '2025-01-01T10:00:00Z', updated_at: '2025-01-02T10:00:00Z', }; describe('PostMapper', () => { it('should convert domain model to view model', () => { const viewPost = postMapper.toViewModel(domainPost); expect(viewPost).toDeepEqual({ id: '1', title: 'Test Post', description: 'This is a test post.', stars: 5, createdAt: new Date('2025-01-01T10:00:00Z'), updatedAt: new Date('2025-01-02T10:00:00Z'), }); }); it('should convert view model back to domain model', () => { const viewPost = postMapper.toViewModel(domainPost); const backToDomain = postMapper.toDomainModel(viewPost); expect(backToDomain).toDeepEqual(domainPost); }); }); ``` --- --- url: /examples/messaging.md --- # Messaging Example: Chat Messages with Attachments (Optional/Nested) This example demonstrates mapping a chat message that may have an optional attachment, showing how to handle optional nested models in mappers. ## 1. Define Domain Models ```ts [domain/models/AttachmentDomainModel.ts] export interface AttachmentDomainModel { id: string; url: string; type: 'image' | 'file'; } ``` ```ts [domain/models/MessageDomainModel.ts] import type { AttachmentDomainModel } from './AttachmentDomainModel'; export interface MessageDomainModel { id: string; sender: string; text: string; sent_at: string; attachment?: AttachmentDomainModel | null; } ``` ## 2. Define View Models ```ts [view/models/AttachmentViewModel.ts] export interface AttachmentViewModel { id: string; url: string; kind: 'image' | 'file'; } ``` ```ts [view/models/MessageViewModel.ts] import type { AttachmentViewModel } from './AttachmentViewModel'; export interface MessageViewModel { id: string; sender: string; text: string; sentAt: Date; attachment?: AttachmentViewModel | null; } ``` ## 3. Define Mappers (Optional/Nested) ```ts [view/mappers/AttachmentMapper.ts] import type { AttachmentDomainModel } from '@/data/domain/models'; import type { AttachmentViewModel } from '../models'; import type { ModelMapper } from './base'; export class AttachmentMapper implements ModelMapper { toViewModel(domain: AttachmentDomainModel): AttachmentViewModel { return { id: domain.id, url: domain.url, kind: domain.type, }; } toDomainModel(view: AttachmentViewModel): AttachmentDomainModel { return { id: view.id, url: view.url, type: view.kind, }; } } ``` ```ts [view/mappers/MessageMapper.ts] import type { MessageDomainModel } from '@/data/domain/models'; import type { MessageViewModel } from '../models'; import { AttachmentMapper } from './AttachmentMapper'; import type { ModelMapper } from './base'; const attachmentMapper = new AttachmentMapper(); export class MessageMapper implements ModelMapper { toViewModel(domain: MessageDomainModel): MessageViewModel { return { id: domain.id, sender: domain.sender, text: domain.text, sentAt: new Date(domain.sent_at), attachment: domain.attachment ? attachmentMapper.toViewModel(domain.attachment) : null, }; } toDomainModel(view: MessageViewModel): MessageDomainModel { return { id: view.id, sender: view.sender, text: view.text, sent_at: view.sentAt.toISOString(), attachment: view.attachment ? attachmentMapper.toDomainModel(view.attachment) : null, }; } } ``` ## 4. Usage Example ```ts [usage-example.ts] import { MessageMapper } from '@/data/view/mappers/MessageMapper'; const messageMapper = new MessageMapper(); const domainMessage = { id: 'm1', sender: 'Bob', text: 'See attached', sent_at: '2025-06-15T12:00:00Z', attachment: { id: 'a1', url: '/img.png', type: 'image' }, }; const viewMessage = messageMapper.toViewModel(domainMessage); // viewMessage.attachment?.kind === 'image' ``` *** This approach is useful for any model with optional or nullable nested objects (e.g., notifications with optional actions, etc.). --- --- url: /examples/project-management.md --- # Project Management Example: Projects with Tasks (Deeply Nested Mapping) This example demonstrates mapping a project with an array of tasks, where each task has an assignee (user). This showcases deep/nested mapping with multiple mappers. ## 1. Define Domain Models ```ts [domain/models/UserDomainModel.ts] export interface UserDomainModel { id: string; name: string; } ``` ```ts [domain/models/TaskDomainModel.ts] import type { UserDomainModel } from './UserDomainModel'; export interface TaskDomainModel { id: string; title: string; completed: boolean; assignee: UserDomainModel; } ``` ```ts [domain/models/ProjectDomainModel.ts] import type { TaskDomainModel } from './TaskDomainModel'; export interface ProjectDomainModel { id: string; name: string; tasks: TaskDomainModel[]; } ``` ## 2. Define View Models ```ts [view/models/UserViewModel.ts] export interface UserViewModel { id: string; displayName: string; } ``` ```ts [view/models/TaskViewModel.ts] import type { UserViewModel } from './UserViewModel'; export interface TaskViewModel { id: string; title: string; isDone: boolean; assignee: UserViewModel; } ``` ```ts [view/models/ProjectViewModel.ts] import type { TaskViewModel } from './TaskViewModel'; export interface ProjectViewModel { id: string; name: string; tasks: TaskViewModel[]; } ``` ## 3. Define Mappers (Deeply Nested) ```ts [view/mappers/UserMapper.ts] import type { UserDomainModel } from '@/data/domain/models'; import type { UserViewModel } from '../models'; import type { ModelMapper } from './base'; export class UserMapper implements ModelMapper { toViewModel(domain: UserDomainModel): UserViewModel { return { id: domain.id, displayName: domain.name, }; } toDomainModel(view: UserViewModel): UserDomainModel { return { id: view.id, name: view.displayName, }; } } ``` ```ts [view/mappers/TaskMapper.ts] import type { TaskDomainModel } from '@/data/domain/models'; import type { TaskViewModel } from '../models'; import { UserMapper } from './UserMapper'; import type { ModelMapper } from './base'; const userMapper = new UserMapper(); export class TaskMapper implements ModelMapper { toViewModel(domain: TaskDomainModel): TaskViewModel { return { id: domain.id, title: domain.title, isDone: domain.completed, assignee: userMapper.toViewModel(domain.assignee), }; } toDomainModel(view: TaskViewModel): TaskDomainModel { return { id: view.id, title: view.title, completed: view.isDone, assignee: userMapper.toDomainModel(view.assignee), }; } } ``` ```ts [view/mappers/ProjectMapper.ts] import type { ProjectDomainModel } from '@/data/domain/models'; import type { ProjectViewModel } from '../models'; import { TaskMapper } from './TaskMapper'; import type { ModelMapper } from './base'; const taskMapper = new TaskMapper(); export class ProjectMapper implements ModelMapper { toViewModel(domain: ProjectDomainModel): ProjectViewModel { return { id: domain.id, name: domain.name, tasks: domain.tasks.map(task => taskMapper.toViewModel(task)), }; } toDomainModel(view: ProjectViewModel): ProjectDomainModel { return { id: view.id, name: view.name, tasks: view.tasks.map(task => taskMapper.toDomainModel(task)), }; } } ``` ## 4. Usage Example ```ts [usage-example.ts] import { ProjectMapper } from '@/data/view/mappers/ProjectMapper'; const projectMapper = new ProjectMapper(); const domainProject = { id: 'p1', name: 'Website Redesign', tasks: [ { id: 't1', title: 'Design homepage', completed: false, assignee: { id: 'u1', name: 'Alice' }, }, ], }; const viewProject = projectMapper.toViewModel(domainProject); // viewProject.tasks[0].assignee.displayName === 'Alice' ``` *** This approach is ideal for any scenario with deeply nested or hierarchical data. --- --- url: /examples/todo-list.md --- # Todo App Example: Simple Flat Model This example demonstrates a basic todo app with a flat model and simple mapping (no nesting). ## 1. Define Domain Model ```ts [domain/models/TodoDomainModel.ts] export interface TodoDomainModel { id: string; text: string; completed: boolean; due_date: string | null; // ISO string or null } ``` ## 2. Define View Model ```ts [view/models/TodoViewModel.ts] export interface TodoViewModel { id: string; text: string; isDone: boolean; dueDate: Date | null; } ``` ## 3. Define Mapper ```ts [view/mappers/TodoMapper.ts] import type { TodoDomainModel } from '@/data/domain/models'; import type { TodoViewModel } from '../models'; import type { ModelMapper } from './base'; export class TodoMapper implements ModelMapper { toViewModel(domain: TodoDomainModel): TodoViewModel { return { id: domain.id, text: domain.text, isDone: domain.completed, dueDate: domain.due_date ? new Date(domain.due_date) : null, }; } toDomainModel(view: TodoViewModel): TodoDomainModel { return { id: view.id, text: view.text, completed: view.isDone, due_date: view.dueDate ? view.dueDate.toISOString() : null, }; } } ``` ## 4. Usage Example ```ts [usage-example.ts] import { TodoMapper } from '@/data/view/mappers/TodoMapper'; const todoMapper = new TodoMapper(); const domainTodo = { id: 't1', text: 'Buy milk', completed: false, due_date: '2025-06-20T12:00:00Z', }; const viewTodo = todoMapper.toViewModel(domainTodo); // viewTodo.isDone === false // viewTodo.dueDate instanceof Date ``` *** This pattern is ideal for simple lists and can be extended for more complex todo apps (e.g., with nested subtasks). --- --- url: /examples/user-management.md --- # User Management Example: Users with Roles (Array Mapping) This example demonstrates mapping a user with an array of roles, showing how to map arrays of nested models. ## 1. Define Domain Models ```ts [domain/models/RoleDomainModel.ts] export interface RoleDomainModel { id: string; name: string; } ``` ```ts [domain/models/UserDomainModel.ts] import type { RoleDomainModel } from './RoleDomainModel'; export interface UserDomainModel { id: string; email: string; display_name: string; roles: RoleDomainModel[]; } ``` ## 2. Define View Models ```ts [view/models/RoleViewModel.ts] export interface RoleViewModel { id: string; label: string; } ``` ```ts [view/models/UserViewModel.ts] import type { RoleViewModel } from './RoleViewModel'; export interface UserViewModel { id: string; email: string; name: string; roles: RoleViewModel[]; } ``` ## 3. Define Mappers (Array Mapping) ```ts [view/mappers/RoleMapper.ts] import type { RoleDomainModel } from '@/data/domain/models'; import type { RoleViewModel } from '../models'; import type { ModelMapper } from './base'; export class RoleMapper implements ModelMapper { toViewModel(domain: RoleDomainModel): RoleViewModel { return { id: domain.id, label: domain.name, }; } toDomainModel(view: RoleViewModel): RoleDomainModel { return { id: view.id, name: view.label, }; } } ``` ```ts [view/mappers/UserMapper.ts] import type { UserDomainModel } from '@/data/domain/models'; import type { UserViewModel } from '../models'; import { RoleMapper } from './RoleMapper'; import type { ModelMapper } from './base'; const roleMapper = new RoleMapper(); export class UserMapper implements ModelMapper { toViewModel(domain: UserDomainModel): UserViewModel { return { id: domain.id, email: domain.email, name: domain.display_name, roles: domain.roles.map(role => roleMapper.toViewModel(role)), }; } toDomainModel(view: UserViewModel): UserDomainModel { return { id: view.id, email: view.email, display_name: view.name, roles: view.roles.map(role => roleMapper.toDomainModel(role)), }; } } ``` ## 4. Usage Example ```ts [usage-example.ts] import { UserMapper } from '@/data/view/mappers/UserMapper'; const userMapper = new UserMapper(); const domainUser = { id: 'u1', email: 'user@example.com', display_name: 'Jane Doe', roles: [ { id: 'r1', name: 'admin' }, { id: 'r2', name: 'editor' }, ], }; const viewUser = userMapper.toViewModel(domainUser); // viewUser.roles[0].label === 'admin' ``` *** This approach is useful for any model with arrays of nested objects (e.g., tags, permissions, etc.). --- --- url: /what-is-dvmm.md --- # What is DVMM? **DVMM (Domain-View Model Mapper)** is a frontend architecture pattern for TypeScript projects. It separates backend data models (domain models) from UI models (view models) using dedicated mappers. This separation leads to cleaner code, easier maintenance, and enables backend and frontend teams to work in parallel. ![DVMM Architecture Diagram](/dvmm-architecture.png) ## Core Concepts * **Domain Model** Represents the backend API response structure. Mirrors the data contract from the server. * **View Model** Represents the data structure needed by the frontend UI. Tailored for UI needs, not backend constraints. * **Mapper** Contains logic to convert between domain and view models, ensuring deterministic and testable data transformations. ## When to use DVMM? DVMM is particularly useful in scenarios where the backend and frontend have different requirements or evolve at different paces. Consider adopting DVMM in the following situations: * **When your backend and frontend evolve independently** In many projects, backend APIs and frontend user interfaces are developed and maintained by separate teams, often on different timelines. DVMM allows you to decouple the backend domain models from the frontend view models, so changes in the backend API (such as adding, removing, or renaming fields) do not immediately break or impact the UI. This separation enables both teams to iterate and deploy independently, reducing bottlenecks and improving overall development velocity. * **When your UI needs data in a different shape than the API provides** The data structures returned by backend APIs are often optimized for storage or transport, not for direct use in the UI. With DVMM, you can define view models that are specifically tailored to the needs of your frontend components, such as grouping related fields, formatting values, or flattening nested structures. This makes it easier to render data, manage UI state, and implement features like sorting, filtering, or pagination without being constrained by the backend’s data shape. * **When you want to enforce type safety and reduce bugs** By explicitly mapping between domain models and view models, DVMM ensures that data transformations are deterministic and predictable. This approach leverages TypeScript’s type system to catch mismatches and errors at compile time, reducing the likelihood of runtime bugs caused by unexpected data shapes or missing fields. Well-defined mappers also make it easier to write unit tests, further increasing the reliability and maintainability of your codebase. * **When working in teams** DVMM fosters better collaboration between backend and frontend developers by clearly defining the boundaries and contracts between the two layers. Backend developers can focus on delivering robust APIs, while frontend developers can work with view models that are optimized for the UI. This parallel workflow minimizes dependencies and blockers, allowing teams to deliver features faster and with greater confidence. ## Best Practices * Keep **Domain Models** strictly aligned with backend. * Shape **View Models** to match the **UI needs**, not the backend. * Keep **Mappers** clean and deterministic. * Do **not** use Domain Models directly in the UI. * Use **unit tests** to validate mapping logic. ## Summary DVMM helps you build scalable, maintainable frontend code by clearly separating backend data contracts from UI needs. Heavily inspired by the MVVM (Model-View-ViewModel) architecture, DVMM adapts its core principles for modern TypeScript projects, emphasizing robust, testable, and future-proof applications. Use DVMM when you want to enforce clear boundaries between backend and frontend, enabling independent evolution and reliable, type-safe code.