Skip to main content

Project Root Structure

simple-manager-mobile/
├── app/                    # Expo Router screens and navigation
├── assets/                 # Static assets (images, fonts)
├── components/             # Shared React components
├── constants/              # App-wide constants
├── hooks/                  # Shared React hooks
├── scripts/                # Build and utility scripts
├── src/                    # Main application code (Clean Architecture)
│   ├── application/        # Business logic layer
│   ├── domain/             # Core entities and types
│   ├── infraestructure/    # Data persistence and external services
│   └── presentation/       # UI-specific code
├── docs/                   # Internal documentation
├── app.json                # Expo configuration
├── package.json            # Dependencies
└── tsconfig.json           # TypeScript configuration

Core Directories

/app - Expo Router

File-based Routing

Expo Router uses file system for navigation
app/
├── (tabs)/                 # Tab navigation group
│   ├── _layout.tsx         # Tab layout configuration
│   ├── index.tsx           # Home tab
│   └── explore.tsx         # Explore tab
├── _layout.tsx             # Root layout
└── modal.tsx               # Modal screen
Files in app/ automatically become routes. Learn more about Expo Router.

/src - Clean Architecture

The src/ directory contains the application’s core logic organized by architectural layers.

Domain Layer

Location

src/domain/
Pure business logic with zero external dependencies.
src/domain/
└── entities/
    ├── Record.ts           # Main entity interface
    ├── Appointment.ts      # Appointment entity
    ├── Service.ts          # Service entity
    └── AppointmentService.ts  # Appointment-Service relation

Example Files

export interface Record {
  id: string;
  title: string;
  subtitle?: string;
  metadata?: string;
  type: string;
  userId?: string;
  createdAt: string;
  updatedAt: string;
  isDeleted: boolean;
}
Domain entities are plain TypeScript interfaces - no classes, no framework dependencies.

Application Layer

Location

src/application/
Business logic orchestration and use cases.
src/application/
├── services/
│   └── RecordService.ts    # Record business logic
├── validators/
│   └── recordValidator.ts  # Validation rules
└── errors/
    └── getErrorMessage.ts  # Error handling utilities

Services

Services contain business logic and coordinate between repositories and UI.
import { Record } from '@/src/domain/entities/Record';
import { RecordRepository } from '@/src/infraestructure/repositories/RecordRepository';
import * as Crypto from 'expo-crypto';

export class RecordService {
  private repository = new RecordRepository();

  async create(title: string, type: string): Promise<Record> {
    const now = new Date().toISOString();
    
    const record: Record = {
      id: Crypto.randomUUID(),
      title,
      type,
      createdAt: now,
      updatedAt: now,
      isDeleted: false,
    };

    await this.repository.create(record);
    return record;
  }

  async list(): Promise<Record[]> {
    return await this.repository.findAll();
  }

  async delete(id: string): Promise<void> {
    await this.repository.softDelete(id);
  }

  async update(record: Record): Promise<void> {
    record.updatedAt = new Date().toISOString();
    await this.repository.update(record);
  }

  async existsByTitle(title: string): Promise<boolean> {
    const records = await this.repository.findAll();
    return records.some(r => r.title.toLowerCase() === title.toLowerCase());
  }
}

Validators

import { Record } from '@/src/domain/entities/Record';

export function validateRecord(record: Partial<Record>): string[] {
  const errors: string[] = [];

  if (!record.title || record.title.trim().length === 0) {
    errors.push('Title is required');
  }

  if (record.title && record.title.length > 200) {
    errors.push('Title must be 200 characters or less');
  }

  if (!record.type) {
    errors.push('Type is required');
  }

  return errors;
}

Error Handling

export function getErrorMessage(error: unknown): string {
  if (error instanceof Error) {
    return error.message;
  }
  
  if (typeof error === 'string') {
    return error;
  }
  
  return 'An unknown error occurred';
}

Infrastructure Layer

Location

src/infraestructure/
External dependencies - database, file system, APIs.
src/infraestructure/
├── database/
│   ├── database.ts         # SQLite connection
│   └── initDatabase.ts     # Database schema initialization
└── repositories/
    └── RecordRepository.ts # Data access layer

Database

import * as SQLite from "expo-sqlite";

export const db = SQLite.openDatabaseSync("simple_manager.db");

Repositories

Repositories abstract database operations.
import { Record } from '../../domain/entities/Record';
import { db } from '../database/database';

export class RecordRepository {
  async create(record: Record): Promise<void> {
    await db.runAsync(
      `INSERT INTO records (
        id, title, subtitle, metadata, type, userId,
        createdAt, updatedAt, isDeleted
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
      [
        record.id,
        record.title,
        record.subtitle ?? null,
        record.metadata ?? null,
        record.type,
        record.userId ?? null,
        record.createdAt,
        record.updatedAt,
        record.isDeleted ? 1 : 0,
      ]
    );
  }

  async findAll(): Promise<Record[]> {
    const rows = await db.getAllAsync<any>(
      `SELECT * FROM records WHERE isDeleted = 0`
    );
    
    return rows.map((r) => ({
      ...r,
      isDeleted: Boolean(r.isDeleted),
    }));
  }

  async softDelete(id: string): Promise<void> {
    await db.runAsync(
      `UPDATE records SET isDeleted = 1 WHERE id = ?`,
      [id]
    );
  }

  async update(record: Record): Promise<void> {
    await db.runAsync(
      `UPDATE records SET
        title = ?,
        subtitle = ?,
        metadata = ?,
        type = ?,
        userId = ?,
        updatedAt = ?
      WHERE id = ?`,
      [
        record.title,
        record.subtitle ?? null,
        record.metadata ?? null,
        record.type,
        record.userId ?? null,
        record.updatedAt,
        record.id,
      ]
    );
  }
}
Always use repositories for database access. Never call db directly from services or UI.

Presentation Layer

Location

src/presentation/
UI-specific code - screens, components, and hooks.
src/presentation/
├── screens/
│   └── RecordsScreen.tsx   # Full screen components
├── components/
│   └── RecordCard.tsx      # UI components
└── hooks/
    └── useRecords.ts       # Custom hooks for data

Custom Hooks

Hooks bridge the UI and application layers.
import { RecordService } from '@/src/application/services/RecordService';
import { Record } from '@/src/domain/entities/Record';
import { useEffect, useMemo, useState } from 'react';

export function useRecords() {
  const service = useMemo(() => new RecordService(), []);
  const [records, setRecords] = useState<Record[]>([]);

  const load = async () => {
    const data = await service.list();
    setRecords(data);
  };

  const create = async (title: string, type: string) => {
    await service.create(title, type);
    await load();
  };

  const update = async (record: Record) => {
    await service.update(record);
    await load();
  };

  const remove = async (id: string) => {
    await service.delete(id);
    await load();
  };

  useEffect(() => {
    load();
  }, []);

  return { records, load, create, update, remove };
}

Components

import { Record } from '@/src/domain/entities/Record';
import { View, Text, StyleSheet } from 'react-native';

interface RecordCardProps {
  record: Record;
  onPress?: () => void;
}

export function RecordCard({ record, onPress }: RecordCardProps) {
  return (
    <View style={styles.card}>
      <Text style={styles.title}>{record.title}</Text>
      {record.subtitle && (
        <Text style={styles.subtitle}>{record.subtitle}</Text>
      )}
      <Text style={styles.type}>{record.type}</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  card: { padding: 16, backgroundColor: '#fff', borderRadius: 8 },
  title: { fontSize: 18, fontWeight: 'bold' },
  subtitle: { fontSize: 14, color: '#666', marginTop: 4 },
  type: { fontSize: 12, color: '#999', marginTop: 8 },
});

Shared Directories

/components - Shared UI Components

components/
├── ui/                     # Reusable UI elements
│   ├── Button.tsx
│   ├── Input.tsx
│   └── Card.tsx
├── context/                # React Context providers
├── themed-text.tsx         # Themed text component
└── themed-view.tsx         # Themed view component

/hooks - Shared Hooks

hooks/
├── use-color-scheme.ts     # Theme detection
├── use-theme-color.ts      # Theme colors
└── use-records.ts          # Data hooks

/constants

constants/
└── Colors.ts               # Color palette

Configuration Files

app.json

Expo configuration:
{
  "expo": {
    "name": "Simple Manager",
    "slug": "simple-manager",
    "version": "1.0.0",
    "orientation": "portrait",
    "platforms": ["ios", "android", "web"],
    "splash": {
      "image": "./assets/splash.png"
    }
  }
}

tsconfig.json

TypeScript configuration:
{
  "extends": "expo/tsconfig.base",
  "compilerOptions": {
    "strict": true,
    "paths": {
      "@/*": ["./*"]
    }
  }
}

package.json

Key dependencies:
{
  "dependencies": {
    "expo": "~54.0.33",
    "expo-router": "~6.0.23",
    "expo-sqlite": "^16.0.10",
    "expo-crypto": "~15.0.8",
    "react": "19.1.0",
    "react-native": "0.81.5",
    "zustand": "^5.0.11"
  }
}

Import Aliases

The project uses @/ as an import alias for the root directory:
// Domain entities
import { Record } from '@/src/domain/entities/Record';

// Services
import { RecordService } from '@/src/application/services/RecordService';

// Repositories
import { RecordRepository } from '@/src/infraestructure/repositories/RecordRepository';

// Hooks
import { useRecords } from '@/src/presentation/hooks/useRecords';

// Components
import { RecordCard } from '@/src/presentation/components/RecordCard';
Using aliases makes imports cleaner and easier to refactor.

File Naming Conventions

  • PascalCase for components and classes: RecordService.ts, RecordCard.tsx
  • camelCase for utilities and hooks: useRecords.ts, getErrorMessage.ts
  • Use .tsx extension for React components
  • Use .ts for non-React TypeScript files
  • Co-locate tests: RecordService.test.ts
  • Use .test.ts or .spec.ts extension

Best Practices

One Entity Per File

Each entity, service, or component gets its own file

Index Exports

Use index.ts to re-export from directories

Colocate Tests

Keep test files next to the code they test

Shared Code

Put reusable code in shared directories

Next Steps

Clean Architecture

Learn about the architectural layers

Data Flow

Understand how data moves through the system

Development Guide

Start building features

API Reference

Explore the API documentation

Build docs developers (and LLMs) love