Skip to main content

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

Input Normalization

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.

Performance

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

Build docs developers (and LLMs) love