Architecture Patterns
Service Initialization
Use useMemo when initializing services in hooks to avoid recreating instances on every render.
const service = useMemo(() => new RecordService(), []);
This ensures the service instance is created once and reused across renders.
Error Handling
Always use the getErrorMessage utility for consistent error handling.
try {
await service.create(title, type);
} catch (error) {
throw new Error(getErrorMessage(error));
}
Data Reloading
After any mutation operation (create, update, delete), reload the data to keep the UI in sync.
const create = async (title: string, type: string) => {
await service.create(title, type);
await load(); // Reload data after creation
};
Entity Design
UUID Identifiers
Use UUID for all entity identifiers instead of auto-incrementing integers.
import * as Crypto from 'expo-crypto';
const record: Record = {
id: Crypto.randomUUID(),
// ...
};
Timestamps
Always include createdAt and updatedAt timestamps in ISO format.
const now = new Date().toISOString();
const record: Record = {
id: Crypto.randomUUID(),
title,
type,
createdAt: now,
updatedAt: now,
isDeleted: false,
};
Soft Deletes
Use soft deletes (isDeleted flag) instead of hard deletes to preserve data history.
async delete(id: string) {
await this.repository.softDelete(id);
}
Validation
Always trim and normalize user input before validation.
const cleanTitle = title.trim();
const cleanType = type.trim();
Validation Results
Return structured validation results with clear error messages.
export interface ValidationResult {
valid: boolean;
message?: string;
}
export function validateRecord(title: string, type: string): ValidationResult {
if (cleanTitle.length < 3) {
return { valid: false, message: "El título debe tener al menos 3 caracteres" };
}
return { valid: true };
}
Length Constraints
Enforce minimum and maximum length constraints at the service layer.
- Minimum: 3 characters for meaningful data
- Maximum: 50 characters for titles to prevent abuse
Hooks Pattern
Return Object Structure
Export a clear interface from hooks with data and action methods.
return {
records, // State data
load, // Read operations
create, // Create operations
update, // Update operations
remove, // Delete operations
existsByTitle, // Query operations
};
Auto-loading Data
Use useEffect to automatically load data when the hook initializes.
useEffect(() => {
load();
}, []);
Repository Pattern
Single Responsibility
Keep repositories focused on database operations only. No business logic.
// ✓ Good - Pure database operation
async create(record: Record) {
await database.runAsync(/* SQL */);
}
// ✗ Bad - Contains business logic
async create(record: Record) {
if (record.title.length < 3) { // This belongs in service layer
throw new Error("Title too short");
}
await database.runAsync(/* SQL */);
}
Method Naming
Use clear, conventional names for repository methods.
create() - Insert new record
findAll() - Retrieve all records
findById() - Retrieve single record
update() - Update existing record
softDelete() - Mark as deleted
hardDelete() - Permanently remove
File Organization
Folder Structure
src/
├── domain/
│ └── entities/ # Core business objects
├── application/
│ ├── services/ # Business logic
│ └── validators/ # Validation rules
├── infrastructure/
│ ├── database/ # Database setup
│ └── repositories/ # Data access
└── presentation/
├── screens/ # Full page components
├── hooks/ # Custom hooks
└── components/ # Reusable UI components
Follow this exact folder structure. Each layer should be clearly separated.
Type Safety
Entity Types
Define strong TypeScript interfaces for all entities.
export interface Record {
id: string;
title: string;
type: string;
createdAt: string;
updatedAt: string;
isDeleted: boolean;
}
Avoid any
Never use any type. Always define proper types or interfaces.
Memoization
Use useMemo for expensive computations or object creation.
Use useCallback for functions passed as props to prevent unnecessary re-renders.
Summary
Following these best practices ensures:
- Clean separation of concerns
- Consistent patterns across the codebase
- Type safety and fewer runtime errors
- Maintainable code that’s easy to understand
- Better performance through proper optimization