Model Layer

The Model layer represents your application's data, business logic, and data sources. It's completely independent of the UI and can be reused across different platforms.

What is the Model Layer?

In MVVM architecture, the Model is responsible for:

  • Data structures - Entities and type definitions
  • Business rules - Validation and domain logic
  • Data sources - API clients and local storage
  • Data persistence - Saving and retrieving data

Folder Structure

typescript
src/
├── domain/               # Business layer
│   ├── entities/         # Type definitions
│   │   ├── user.ts
│   │   ├── post.ts
│   │   └── product.ts
│   └── schemas/          # Validation schemas
│       ├── user.schema.ts
│       ├── post.schema.ts
│       └── product.schema.ts
│
└── data/                 # Data layer
    ├── api/              # HTTP clients
    │   ├── client.ts
    │   └── endpoints/
    │       ├── users.ts
    │       ├── posts.ts
    │       └── products.ts
    └── storage/          # Local persistence
        ├── mmkv.ts
        ├── user.storage.ts
        └── app.storage.ts

1. Entities (Domain)

Entities are TypeScript interfaces that define the shape of your data. They live in src/domain/entities/.

Example: User Entity

src/domain/entities/user.ts
export interface User {
  id: string;
  name: string;
  email: string;
  avatar?: string;
  createdAt: Date;
  updatedAt: Date;
}

export interface UserProfile extends User {
  bio: string;
  website?: string;
  socialLinks: {
    twitter?: string;
    github?: string;
    linkedin?: string;
  };
  followers: number;
  following: number;
}

export interface CreateUserInput {
  name: string;
  email: string;
  password: string;
}

export interface UpdateUserInput {
  name?: string;
  email?: string;
  avatar?: string;
  bio?: string;
}
Entities are pure TypeScript interfaces with no implementation. They represent your domain model.

2. Schemas (Validation)

Schemas use Zod to validate data at runtime. They ensure type safety beyond TypeScript's compile-time checks.

Example: User Schema

src/domain/schemas/user.schema.ts
import { z } from 'zod';

export const userSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(2).max(50),
  email: z.string().email(),
  avatar: z.string().url().optional(),
  createdAt: z.date(),
  updatedAt: z.date(),
});

export const createUserSchema = z.object({
  name: z.string()
    .min(2, 'Name must be at least 2 characters')
    .max(50, 'Name must be less than 50 characters'),
  email: z.string().email('Invalid email address'),
  password: z.string()
    .min(8, 'Password must be at least 8 characters')
    .regex(/[A-Z]/, 'Password must contain an uppercase letter')
    .regex(/[0-9]/, 'Password must contain a number'),
});

export const updateUserSchema = z.object({
  name: z.string().min(2).max(50).optional(),
  email: z.string().email().optional(),
  avatar: z.string().url().optional(),
  bio: z.string().max(200).optional(),
}).strict();

// Infer TypeScript types from schemas
export type UserSchemaType = z.infer<typeof userSchema>;
export type CreateUserInput = z.infer<typeof createUserSchema>;
export type UpdateUserInput = z.infer<typeof updateUserSchema>;
Zod schemas provide both runtime validation and TypeScript type inference.

3. API Layer (Data Access)

API clients handle communication with remote servers. They live in src/data/api/.

Example: API Client Setup

src/data/api/axios.client.ts
import { createHttpClient, IHttpClient } from '@/data/api';
import { IStorage } from '@/data/storage';

// O httpClient é criado via factory no bootstrap
export const createHttpClient = (storage: IStorage, baseURL: string): IHttpClient => {
  // Implementação com Axios
  // - Adiciona token automaticamente via interceptor
  // - Trata refresh token em 401
  // - Normaliza erros
  // Ver: src/data/api/axios.client.ts
};

// Uso no código:
import { getHttpClient } from '@/core/config';

const httpClient = getHttpClient();

// Define token após login
httpClient.setAuthToken(token);

// Remove token no logout
httpClient.setAuthToken(null);

// Muda base URL se necessário
httpClient.setBaseUrl('https://api.staging.com');

Example: Users API Endpoint

src/data/api/endpoints/users.ts
import { getHttpClient } from '@/core/config';
import type { User, UserProfile, CreateUserInput, UpdateUserInput } from '@/domain/entities/user';

export const userApi = {
  // Get user by ID
  getUser: async (id: string): Promise<User> => {
    const httpClient = getHttpClient();
    const { data } = await httpClient.get<User>(`/users/${id}`);
    return data;
  },

  // Get user profile
  getProfile: async (id: string): Promise<UserProfile> => {
    const httpClient = getHttpClient();
    const { data } = await httpClient.get<UserProfile>(`/users/${id}/profile`);
    return data;
  },

  // Create user
  createUser: async (input: CreateUserInput): Promise<User> => {
    const httpClient = getHttpClient();
    const { data } = await httpClient.post<User, CreateUserInput>('/users', input);
    return data;
  },

  // Update user
  updateUser: async (id: string, input: UpdateUserInput): Promise<User> => {
    const httpClient = getHttpClient();
    const { data } = await httpClient.put<User, UpdateUserInput>(`/users/${id}`, input);
    return data;
  },

  // Delete user
  deleteUser: async (id: string): Promise<void> => {
    const httpClient = getHttpClient();
    await httpClient.delete<void>(`/users/${id}`);
  },

  // Search users
  searchUsers: async (query: string): Promise<User[]> => {
    const httpClient = getHttpClient();
    const { data } = await httpClient.get<User[]>('/users/search', {
      params: { q: query },
    });
    return data;
  },
};

4. Storage Layer (Local Persistence)

Storage utilities handle local data persistence using MMKV.

Example: User Storage

src/data/storage/user.storage.ts
import { storage } from './mmkv';
import type { User } from '@/domain/entities/user';

export const userStorage = {
  // Save current user
  setCurrentUser: (user: User) => {
    storage.set('current_user', JSON.stringify(user));
  },

  // Get current user
  getCurrentUser: (): User | null => {
    const data = storage.getString('current_user');
    return data ? JSON.parse(data) : null;
  },

  // Remove current user
  clearCurrentUser: () => {
    storage.delete('current_user');
  },

  // Save auth token
  setAuthToken: (token: string) => {
    storage.set('auth_token', token);
  },

  // Get auth token
  getAuthToken: (): string | null => {
    return storage.getString('auth_token') || null;
  },

  // Clear auth data
  clearAuth: () => {
    storage.delete('auth_token');
    storage.delete('current_user');
  },
};
Never store sensitive data like passwords in local storage. Only store tokens and non-sensitive user data.

Model Layer Best Practices

✅ Keep it pure

The Model should have no dependencies on UI or presentation logic.

✅ Use TypeScript interfaces

Define clear interfaces for all entities to ensure type safety.

✅ Validate with Zod

Use Zod schemas for runtime validation, especially for API responses and user input.

✅ Separate concerns

Keep entities, schemas, API logic, and storage separate for better maintainability.

Complete Example

Here's how all pieces work together:

Complete workflow
// 1. Define entity
// src/domain/entities/post.ts
export interface Post {
  id: string;
  title: string;
  content: string;
  authorId: string;
  createdAt: Date;
}

// 2. Create schema
// src/domain/schemas/post.schema.ts
import { z } from 'zod';

export const createPostSchema = z.object({
  title: z.string().min(5).max(100),
  content: z.string().min(10),
});

// 3. Build API client
// src/data/api/endpoints/posts.ts
export const postApi = {
  getPosts: async (): Promise<Post[]> => {
    const { data } = await apiClient.get('/posts');
    return data;
  },
  
  createPost: async (input: CreatePostInput): Promise<Post> => {
    // Validate before sending
    createPostSchema.parse(input);
    const { data } = await apiClient.post('/posts', input);
    return data;
  },
};

// 4. Use in ViewModel (next layer)
// The ViewModel will use these Model components
// to manage data and expose it to the View

Next Steps