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
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.ts1. Entities (Domain)
Entities are TypeScript interfaces that define the shape of your data. They live in src/domain/entities/.
Example: User Entity
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;
}2. Schemas (Validation)
Schemas use Zod to validate data at runtime. They ensure type safety beyond TypeScript's compile-time checks.
Example: User Schema
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>;3. API Layer (Data Access)
API clients handle communication with remote servers. They live in src/data/api/.
Example: API Client Setup
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
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
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');
},
};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:
// 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