Project Structure
Complete guide to the Caracal starter project structure, file organization, and architectural patterns.
The Caracal starter follows a well-organized, scalable structure designed for production React Native applications. This guide explains the purpose of each directory and how they work together.
Overview
If you open the project in your editor, you'll see this structure:
src/
├── api/ # API layer and data fetching
│ ├── common/ # Shared API utilities
│ │ ├── api-provider.tsx
│ │ └── client.tsx
│ ├── index.tsx
│ └── types.ts
├── app/ # Expo Router routes (file-based routing)
│ ├── (components)/ # 30 component showcase routes
│ │ ├── _layout.tsx
│ │ ├── accordion.tsx
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ ├── component-list.tsx
│ │ └── ...
│ ├── (home)/ # Main app routes
│ │ ├── _layout.tsx
│ │ ├── index.tsx
│ │ └── settings.tsx
│ ├── _layout.tsx # Root layout with providers
│ ├── +html.tsx
│ └── [...messing].tsx # 404 catch-all route
├── components/ # Reusable components
│ ├── ui/ # Core UI components (custom)
│ │ ├── focus-aware-status-bar.tsx
│ │ ├── image.tsx
│ │ ├── index.tsx
│ │ ├── input.tsx # ControlledInput
│ │ ├── list.tsx # FlashList wrapper
│ │ ├── modal.tsx
│ │ ├── text.tsx # i18n-enabled Text
│ │ ├── utils.tsx
│ │ └── icons/
│ ├── theme-toggle.tsx
│ └── logo.tsx
├── lib/ # Core utilities and configuration
│ ├── auth/
│ │ ├── index.tsx
│ │ └── utils.tsx
│ ├── contexts/
│ │ └── app-theme-context.tsx
│ ├── hooks/
│ │ ├── index.tsx
│ │ ├── use-is-first-time.tsx
│ │ └── use-selected-theme.tsx
│ ├── i18n/
│ │ ├── index.tsx
│ │ ├── resources.ts
│ │ └── react-i18next.d.ts
│ ├── env.js # Client env re-export
│ ├── storage.tsx # MMKV wrapper
│ ├── test-utils.tsx
│ └── utils.ts
├── themes/ # Uniwind theme tokens (CSS)
│ └── sky.css
├── translations/ # i18n resource files
│ ├── ar.json
│ └── en.json
└── types/ # Global TypeScript types
├── index.ts
└── image.d.ts
env.js # Environment variable validation (Zod)
app.config.ts # Expo configuration
package.json
tsconfig.json
global.css # Tailwind + Uniwind CSS entry
uniwind-types.d.ts # Auto-generated Uniwind typings
babel.config.js
metro.config.js
jest.config.js
eslint.config.mjsDirectory Deep Dive
src/app/ - File-Based Routing
The app/ directory contains all routes using Expo Router with file-based routing and typed routes enabled.
Route Groups
The project uses route groups (directories wrapped in parentheses) to organize routes without affecting URLs:
(home) - Main Application Routes
Contains the primary app screens with native tab navigation:
_layout.tsx- Tab navigator usingexpo-router/unstable-native-tabsindex.tsx- Home/Feed screen (main landing page)settings.tsx- Settings screen
Navigation: These appear as tabs at the bottom of the app.
(components) - Component Showcase Routes
Contains 30 interactive component examples demonstrating HeroUI Native components:
_layout.tsx- Stack navigator for component screens- 29 component showcase screens (accordion, button, card, etc.)
component-list.tsx- Navigation hub for all showcases
Purpose: Educational resource and reference implementation for developers learning HeroUI Native components.
The (components) route group serves as a living documentation system. Run the app and explore these screens to see HeroUI Native components in action with real code examples.
Root Layout (_layout.tsx)
The root layout establishes the provider hierarchy critical for the app to function:
// Provider order (inside to outside):
// 1. GestureHandlerRootView - Gesture support
// 2. KeyboardProvider - Keyboard handling
// 3. AppThemeProvider - Theme management (light/dark)
// 4. HeroUINativeProvider - UI component context
// 5. APIProvider - React Query setup
// 6. BottomSheetModalProvider - Bottom sheet supportsrc/components/ - Reusable Components
components/ui/ - Custom UI Components
Contains 6 custom components designed for specific needs:
| Component | Purpose | File |
|---|---|---|
| Image | expo-image with Uniwind support | image.tsx |
| Text | i18n-enabled with RTL support | text.tsx |
| ControlledInput | Form input with react-hook-form | input.tsx |
| List | FlashList wrapper with EmptyList | list.tsx |
| Modal | Bottom sheet modal wrapper | modal.tsx |
| FocusAwareStatusBar | Route-aware StatusBar | focus-aware-status-bar.tsx |
Important: For buttons, cards, dialogs, and other standard UI, use HeroUI Native components instead.
src/api/ - Data Fetching Layer
API calls organized using React Query with react-query-kit for type safety:
api/
├── common/
│ ├── api-provider.tsx # QueryClient provider
│ └── client.tsx # Axios instance (baseURL from Env.API_URL)
├── index.tsx # API exports
└── types.ts # API type definitionsPattern:
import { createQuery } from 'react-query-kit';
import { client } from './common/client';
export const usePosts = createQuery({
queryKey: ['posts'],
fetcher: () => client.get('/posts'),
});See Data Fetching guide for complete patterns.
src/lib/ - Core Utilities
The lib/ directory contains framework-agnostic utilities that can be shared across projects.
lib/auth/ - Authentication State
Zustand-based auth state with MMKV persistence:
// src/lib/auth/index.tsx
type AuthState = {
token: TokenType | null;
status: 'idle' | 'signOut' | 'signIn';
signIn: (data: TokenType) => void;
signOut: () => void;
hydrate: () => void;
};
// Usage with selectors pattern
const token = useAuth.use.token();
const signIn = useAuth.use.signIn();See Authentication guide for details.
lib/hooks/ - Custom Hooks
use-is-first-time.tsx- First-time user detectionuse-selected-theme.tsx- Current theme selectionuse-accessability-info.ts- Accessibility utilities
lib/i18n/ - Internationalization
i18next configuration and TypeScript types:
i18n/
├── index.tsx # i18next setup
├── resources.ts # Translation imports
└── react-i18next.d.ts # TypeScript definitionsSee Internationalization guide for usage.
lib/storage.tsx - MMKV Storage
Type-safe wrapper around react-native-mmkv:
import { getItem, setItem, removeItem } from '@/lib/storage';
// Persist data
await setItem('user', { id: 1, name: 'John' });
// Retrieve data
const user = getItem<User>('user');
// Remove data
await removeItem('user');lib/env.js - Environment Variables
Re-exports client environment variables from root env.js:
import { Env } from '@env'; // Resolves to src/lib/env.js
const apiUrl = Env.API_URL;
const appName = Env.NAME;See Environment Variables guide for configuration.
lib/utils.ts - General Utilities
Shared utilities including:
createSelectors- Zustand selectors pattern for performance- Other helper functions
src/translations/ - Translation Resources
JSON files for each supported locale:
translations/
├── en.json # English translations
└── ar.json # Arabic translationsFormat:
{
"welcome": {
"title": "Welcome to Caracal",
"subtitle": "A React Native starter"
}
}Validation: ESLint ensures:
- Identical keys across all language files
- Sorted keys alphabetically
- Valid JSON syntax
src/types/ - Global TypeScript Types
Shared type definitions used across the app:
types/
├── index.ts # General types
└── image.d.ts # Image module declarationsRoot Configuration Files
env.js - Environment Variable System
Critical file that loads and validates environment variables using Zod schemas:
// Defines two schemas:
const client = z.object({
APP_ENV: z.enum(['development', 'staging', 'production']),
API_URL: z.string(),
// ... client variables
});
const buildTime = z.object({
EXPO_ACCOUNT_OWNER: z.string(),
EAS_PROJECT_ID: z.string(),
// ... build-time only variables
});Environment files: .env.development, .env.staging, .env.production
See Environment Variables guide for complete setup.
app.config.ts - Expo Configuration
Dynamic Expo configuration with:
- Multi-environment support (dev/staging/prod)
- React Compiler enabled (experimental)
- Typed routes enabled
- App icon badges for non-production builds
experiments: {
typedRoutes: true, // Type-safe navigation
reactCompiler: true, // Automatic optimization
}
scheme: 'caracalApp', // Deep linkingThe New Architecture is always enabled on Expo SDK 55+ and has no config flag.
global.css - Uniwind Configuration
Uniwind v1.10.0 uses TailwindCSS v4, which is configured in CSS rather than a tailwind.config.js file. global.css is the entry point:
@import "tailwindcss";
@import "uniwind";
@import "heroui-native/styles";
@source './node_modules/heroui-native/lib';
@import "./src/themes/sky.css";Design tokens (colors, fonts, radius) live in src/themes/sky.css as CSS custom properties, split into @variant light and @variant dark blocks.
Metro is what wires this up, in metro.config.js:
module.exports = withUniwindConfig(config, {
cssEntryFile: './global.css',
dtsFile: './uniwind-types.d.ts',
});See UI & Theming guide for styling.
Testing & Quality
jest.config.js- Jest configuration with jest-expo preseteslint.config.mjs- ESLint flat config (v9) with React Compiler plugin.prettierrc.js- Code formatting with TailwindCSS plugin
Key Architectural Patterns
1. Route Groups for Organization
Route groups organize routes without affecting URLs:
app/
├── (home)/ # Tab navigation
│ └── index.tsx # URL: /
└── (components)/ # Stack navigation
└── button.tsx # URL: /button (not /components/button)2. Absolute Imports
All imports use @/ prefix for cleaner code:
// ✅ Good
import { Image, Text } from '@/components/ui';
import { useAuth } from '@/lib/auth';
import { usePosts } from '@/api';
// ❌ Avoid
import { Image } from '../../../components/ui';Configuration: Set up in tsconfig.json and babel.config.js.
3. Provider Hierarchy
The app wraps components in a specific order (see src/app/_layout.tsx):
GestureHandlerRootView
└─ KeyboardProvider
└─ AppThemeProvider
└─ HeroUINativeProvider
└─ APIProvider (React Query)
└─ BottomSheetModalProvider
└─ App ContentWhy this order?
- Gestures must wrap everything
- Keyboard handling before UI
- Theme before styled components
- API provider before data-fetching components
- Bottom sheet last for modals to overlay everything
4. Component Showcase System
The (components) route group provides:
- 30 interactive examples of HeroUI Native components
- Live demonstrations with code patterns
- Learning resource for developers
- Reference implementation for component usage
How to use:
- Run the app:
pnpm start - Navigate to Components section
- Explore each component showcase
- View source code in
src/app/(components)/
5. Separation of Concerns
| Directory | Purpose | Import From |
|---|---|---|
components/ui/ | Custom components | @/components/ui |
| HeroUI Native | Standard UI library | heroui-native |
lib/ | Framework-agnostic utils | @/lib/* |
api/ | Data fetching | @/api |
app/ | Routes only | Don't import from here |
Common Use Cases
Adding a New Screen
-
Create file in appropriate route group:
src/app/(home)/profile.tsx -
Export default component:
export default function ProfileScreen() { return <View>...</View>; } -
Navigation handled automatically by Expo Router
Adding a New Component
- For standard UI: Use HeroUI Native components
- For custom needs: Create in
src/components/ - For reusable UI utilities: Add to
src/components/ui/
Adding a New API Endpoint
-
Create in
src/api/:// src/api/users.ts export const useUsers = createQuery({ queryKey: ['users'], fetcher: () => client.get('/users'), }); -
Export from
src/api/index.tsx -
Use in components:
import { useUsers } from '@/api'; const { data, isLoading } = useUsers();
Project Philosophy
The structure follows these principles:
- Production-ready: Organized for scale and maintainability
- Clear separation: Each directory has a single responsibility
- Reusability: Lib utilities are framework-agnostic
- Living documentation: Component showcases as learning tool
- Type safety: TypeScript throughout with strict mode
- Minimal dependencies: Only essential, well-maintained libraries
Opinionated but flexible: This structure represents best practices for React Native development. Feel free to adapt it to your project's needs, but the core organization principles should remain.
What's Next?
Now that you understand the project structure, explore:
- Environment Variables - Multi-environment setup
- Rules and Conventions - Code standards
- Navigation - Routing and navigation patterns
Quick Reference:
# View file tree in terminal
tree -L 3 -I 'node_modules|.expo|android|ios|dist'
# Count component showcases
ls src/app/\(components\)/*.tsx | wc -l # 30 files