Skip to main content

Documentation Index

Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt

Use this file to discover all available pages before exploring further.

Project Structure

Module Overview

Estimated Time: 2 hours | Difficulty: Beginner-Intermediate | Prerequisites: Environment setup complete
A well-organized project structure is crucial for maintainability, especially as your app grows. Mobile projects have a unique structural challenge that web projects do not: you have platform-specific native directories (ios/ and android/) alongside your JavaScript source code, configuration files for multiple build systems (Gradle, Xcode, Metro, Babel), and assets that may need different resolutions for different screen densities. Without a clear structure, navigating between these layers becomes painful fast. This module covers industry-standard patterns used by companies like Shopify, Discord, and Coinbase. What You’ll Learn:
  • Default project structure (Expo and CLI)
  • Feature-based folder organization
  • Barrel exports and module organization
  • Configuration management
  • Environment variables
  • Path aliases and absolute imports

Default Project Structures

Expo Project Structure

my-expo-app/
├── app/                      # Expo Router screens (file-based routing)
│   ├── (tabs)/              # Tab group
│   │   ├── _layout.tsx      # Tab bar configuration
│   │   ├── index.tsx        # Home tab (/)
│   │   └── explore.tsx      # Explore tab (/explore)
│   ├── _layout.tsx          # Root layout
│   ├── +not-found.tsx       # 404 screen
│   └── modal.tsx            # Modal screen
├── assets/                   # Static assets
│   ├── fonts/               # Custom fonts
│   └── images/              # Images and icons
├── components/              # Reusable components
│   ├── ui/                  # UI primitives
│   └── ThemedText.tsx
├── constants/               # App constants
│   └── Colors.ts
├── hooks/                   # Custom hooks
│   └── useColorScheme.ts
├── .expo/                   # Expo cache (gitignored)
├── node_modules/            # Dependencies (gitignored)
├── app.json                 # Expo configuration
├── babel.config.js          # Babel configuration
├── package.json             # Dependencies and scripts
├── tsconfig.json            # TypeScript configuration
└── .gitignore

React Native CLI Structure

my-cli-app/
├── android/                  # Android native code
│   ├── app/
│   │   ├── src/main/
│   │   │   ├── java/        # Java/Kotlin code
│   │   │   └── res/         # Android resources
│   │   └── build.gradle
│   ├── gradle/
│   └── settings.gradle
├── ios/                      # iOS native code
│   ├── MyApp/
│   │   ├── AppDelegate.mm
│   │   ├── Info.plist
│   │   └── Images.xcassets
│   ├── MyApp.xcodeproj
│   ├── MyApp.xcworkspace
│   └── Podfile
├── src/                      # Your app code
│   ├── components/
│   ├── screens/
│   └── App.tsx
├── __tests__/               # Test files
├── node_modules/
├── .bundle/
├── index.js                 # Entry point
├── app.json
├── babel.config.js
├── metro.config.js
├── package.json
└── tsconfig.json

For production apps, use a feature-based structure. The key insight: organize by business domain (auth, products, cart), not by technical role (components, screens, hooks). When a new developer joins and is asked to “fix the cart total calculation,” they should open one folder, not hunt across five. This pattern also maps naturally to team ownership — one squad owns the features/cart/ directory, another owns features/products/. A mobile-specific consideration: features often need platform-specific implementations. Rather than creating top-level ios/ and android/ source directories, use React Native’s file-extension convention (e.g., PaymentButton.ios.tsx and PaymentButton.android.tsx) within the feature folder. This keeps platform differences co-located with the feature they belong to.
src/
├── app/                      # App entry and providers
│   ├── App.tsx              # Root component
│   ├── providers/           # Context providers
│   │   ├── AuthProvider.tsx
│   │   ├── ThemeProvider.tsx
│   │   └── index.tsx
│   └── navigation/          # Navigation configuration
│       ├── RootNavigator.tsx
│       ├── AuthNavigator.tsx
│       ├── MainNavigator.tsx
│       └── linking.ts

├── features/                 # Feature modules
│   ├── auth/                # Authentication feature
│   │   ├── components/
│   │   │   ├── LoginForm.tsx
│   │   │   └── SignupForm.tsx
│   │   ├── screens/
│   │   │   ├── LoginScreen.tsx
│   │   │   ├── SignupScreen.tsx
│   │   │   └── ForgotPasswordScreen.tsx
│   │   ├── hooks/
│   │   │   └── useAuth.ts
│   │   ├── services/
│   │   │   └── authService.ts
│   │   ├── store/
│   │   │   └── authSlice.ts
│   │   ├── types/
│   │   │   └── auth.types.ts
│   │   └── index.ts         # Barrel export
│   │
│   ├── products/            # Products feature
│   │   ├── components/
│   │   │   ├── ProductCard.tsx
│   │   │   ├── ProductList.tsx
│   │   │   └── ProductFilters.tsx
│   │   ├── screens/
│   │   │   ├── ProductsScreen.tsx
│   │   │   └── ProductDetailScreen.tsx
│   │   ├── hooks/
│   │   │   ├── useProducts.ts
│   │   │   └── useProductFilters.ts
│   │   ├── services/
│   │   │   └── productService.ts
│   │   ├── store/
│   │   │   └── productSlice.ts
│   │   ├── types/
│   │   │   └── product.types.ts
│   │   └── index.ts
│   │
│   ├── cart/                # Cart feature
│   │   ├── components/
│   │   ├── screens/
│   │   ├── hooks/
│   │   ├── store/
│   │   └── index.ts
│   │
│   └── profile/             # Profile feature
│       ├── components/
│       ├── screens/
│       ├── hooks/
│       └── index.ts

├── shared/                   # Shared code across features
│   ├── components/          # Reusable UI components
│   │   ├── ui/              # Primitive components
│   │   │   ├── Button.tsx
│   │   │   ├── Input.tsx
│   │   │   ├── Text.tsx
│   │   │   ├── Card.tsx
│   │   │   └── index.ts
│   │   ├── layout/          # Layout components
│   │   │   ├── Container.tsx
│   │   │   ├── SafeArea.tsx
│   │   │   └── index.ts
│   │   └── feedback/        # Feedback components
│   │       ├── Loading.tsx
│   │       ├── ErrorBoundary.tsx
│   │       ├── Toast.tsx
│   │       └── index.ts
│   │
│   ├── hooks/               # Shared hooks
│   │   ├── useDebounce.ts
│   │   ├── useKeyboard.ts
│   │   ├── useNetworkStatus.ts
│   │   └── index.ts
│   │
│   ├── services/            # Shared services
│   │   ├── api/
│   │   │   ├── client.ts    # API client (Axios/fetch)
│   │   │   ├── interceptors.ts
│   │   │   └── index.ts
│   │   ├── storage/
│   │   │   ├── secureStorage.ts
│   │   │   ├── asyncStorage.ts
│   │   │   └── index.ts
│   │   └── analytics/
│   │       └── analytics.ts
│   │
│   ├── utils/               # Utility functions
│   │   ├── formatting.ts
│   │   ├── validation.ts
│   │   ├── date.ts
│   │   └── index.ts
│   │
│   ├── constants/           # App constants
│   │   ├── colors.ts
│   │   ├── spacing.ts
│   │   ├── typography.ts
│   │   ├── config.ts
│   │   └── index.ts
│   │
│   └── types/               # Shared TypeScript types
│       ├── navigation.types.ts
│       ├── api.types.ts
│       ├── common.types.ts
│       └── index.ts

├── assets/                   # Static assets
│   ├── fonts/
│   ├── images/
│   ├── icons/
│   └── animations/          # Lottie files

└── __tests__/               # Test utilities
    ├── setup.ts
    ├── mocks/
    └── utils/

Feature Module Pattern

Each feature is a self-contained module with everything it needs. Think of each feature folder as a mini-app: it owns its screens, components, hooks, services, types, and state. The benefit is isolation — a developer working on the “cart” feature does not need to understand the “auth” feature’s internals, and deleting a feature means removing one folder rather than hunting through a dozen shared directories. This pattern becomes especially valuable in React Native because features often have platform-specific behavior (e.g., Apple Pay on iOS vs Google Pay on Android) that stays contained within the feature folder rather than leaking into shared code.
// src/features/products/index.ts (Barrel Export)
// Export only what other features need

// Screens (for navigation)
export { ProductsScreen } from './screens/ProductsScreen';
export { ProductDetailScreen } from './screens/ProductDetailScreen';

// Components (if shared)
export { ProductCard } from './components/ProductCard';

// Hooks (if shared)
export { useProducts } from './hooks/useProducts';

// Types (if shared)
export type { Product, ProductCategory } from './types/product.types';

// Store (for root store)
export { productSlice, productReducer } from './store/productSlice';

Feature Structure Example

// src/features/products/types/product.types.ts
export interface Product {
  id: string;
  name: string;
  description: string;
  price: number;
  images: string[];
  category: ProductCategory;
  inStock: boolean;
  rating: number;
  reviewCount: number;
}

export interface ProductCategory {
  id: string;
  name: string;
  slug: string;
}

export interface ProductFilters {
  category?: string;
  minPrice?: number;
  maxPrice?: number;
  inStock?: boolean;
  sortBy?: 'price' | 'rating' | 'newest';
}
// src/features/products/services/productService.ts
import { apiClient } from '@/shared/services/api';
import type { Product, ProductFilters } from '../types/product.types';

export const productService = {
  async getProducts(filters?: ProductFilters): Promise<Product[]> {
    const response = await apiClient.get('/products', { params: filters });
    return response.data;
  },

  async getProductById(id: string): Promise<Product> {
    const response = await apiClient.get(`/products/${id}`);
    return response.data;
  },

  async searchProducts(query: string): Promise<Product[]> {
    const response = await apiClient.get('/products/search', {
      params: { q: query },
    });
    return response.data;
  },
};
// src/features/products/hooks/useProducts.ts
import { useQuery } from '@tanstack/react-query';
import { productService } from '../services/productService';
import type { ProductFilters } from '../types/product.types';

export function useProducts(filters?: ProductFilters) {
  return useQuery({
    queryKey: ['products', filters],
    queryFn: () => productService.getProducts(filters),
    staleTime: 5 * 60 * 1000, // 5 minutes
  });
}

export function useProduct(id: string) {
  return useQuery({
    queryKey: ['product', id],
    queryFn: () => productService.getProductById(id),
    enabled: !!id,
  });
}
// src/features/products/components/ProductCard.tsx
import { View, Text, Image, Pressable, StyleSheet } from 'react-native';
import type { Product } from '../types/product.types';

interface ProductCardProps {
  product: Product;
  onPress: (product: Product) => void;
}

export function ProductCard({ product, onPress }: ProductCardProps) {
  return (
    <Pressable
      style={styles.container}
      onPress={() => onPress(product)}
    >
      <Image
        source={{ uri: product.images[0] }}
        style={styles.image}
        resizeMode="cover"
      />
      <View style={styles.content}>
        <Text style={styles.name} numberOfLines={2}>
          {product.name}
        </Text>
        <Text style={styles.price}>
          ${product.price.toFixed(2)}
        </Text>
        <View style={styles.rating}>
          <Text style={styles.ratingText}>
{product.rating} ({product.reviewCount})
          </Text>
        </View>
      </View>
    </Pressable>
  );
}

const styles = StyleSheet.create({
  container: {
    backgroundColor: '#fff',
    borderRadius: 12,
    overflow: 'hidden',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
    elevation: 3,
  },
  image: {
    width: '100%',
    height: 150,
  },
  content: {
    padding: 12,
  },
  name: {
    fontSize: 16,
    fontWeight: '600',
    color: '#1f2937',
    marginBottom: 4,
  },
  price: {
    fontSize: 18,
    fontWeight: '700',
    color: '#3b82f6',
    marginBottom: 4,
  },
  rating: {
    flexDirection: 'row',
    alignItems: 'center',
  },
  ratingText: {
    fontSize: 14,
    color: '#6b7280',
  },
});

Barrel Exports

Barrel exports simplify imports and create a clean public API for each module:
// src/shared/components/ui/index.ts
export { Button } from './Button';
export { Input } from './Input';
export { Text } from './Text';
export { Card } from './Card';
export { Avatar } from './Avatar';
export { Badge } from './Badge';

// Types
export type { ButtonProps } from './Button';
export type { InputProps } from './Input';
// src/shared/hooks/index.ts
export { useDebounce } from './useDebounce';
export { useKeyboard } from './useKeyboard';
export { useNetworkStatus } from './useNetworkStatus';
export { useAppState } from './useAppState';
export { usePrevious } from './usePrevious';

Using Barrel Exports

// Clean imports from barrel exports
import { Button, Input, Card } from '@/shared/components/ui';
import { useDebounce, useNetworkStatus } from '@/shared/hooks';
import { ProductCard, useProducts } from '@/features/products';

// Instead of:
import { Button } from '@/shared/components/ui/Button';
import { Input } from '@/shared/components/ui/Input';
import { Card } from '@/shared/components/ui/Card';

Path Aliases

Configure path aliases for cleaner imports:

TypeScript Configuration

// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@/app/*": ["src/app/*"],
      "@/features/*": ["src/features/*"],
      "@/shared/*": ["src/shared/*"],
      "@/assets/*": ["src/assets/*"]
    }
  }
}

Babel Configuration

// babel.config.js
module.exports = function (api) {
  api.cache(true);
  return {
    presets: ['babel-preset-expo'], // or 'module:metro-react-native-babel-preset'
    plugins: [
      [
        'module-resolver',
        {
          root: ['./src'],
          alias: {
            '@': './src',
            '@/app': './src/app',
            '@/features': './src/features',
            '@/shared': './src/shared',
            '@/assets': './src/assets',
          },
        },
      ],
    ],
  };
};
Install the plugin:
npm install --save-dev babel-plugin-module-resolver

Usage

// Before (relative imports)
import { Button } from '../../../shared/components/ui/Button';
import { useAuth } from '../../auth/hooks/useAuth';

// After (path aliases)
import { Button } from '@/shared/components/ui';
import { useAuth } from '@/features/auth';

Environment Variables

Managing environment variables in React Native is more nuanced than in web development. On the web, tools like dotenv inject values at build time and you are done. In React Native, you need to consider: (1) JavaScript-side variables for your React code, (2) native-side variables for iOS Info.plist and Android build.gradle configurations, and (3) the fact that secrets bundled into a mobile app can be extracted by anyone who decompiles the binary. Never put truly secret API keys directly in your JS bundle — use a backend proxy instead.

Using Expo

# .env
API_URL=https://api.example.com
API_KEY=your-api-key
ENVIRONMENT=development
// app.config.ts
import 'dotenv/config';

export default {
  expo: {
    name: 'My App',
    slug: 'my-app',
    extra: {
      apiUrl: process.env.API_URL,
      apiKey: process.env.API_KEY,
      environment: process.env.ENVIRONMENT,
    },
  },
};
// src/shared/constants/config.ts
import Constants from 'expo-constants';

export const config = {
  apiUrl: Constants.expoConfig?.extra?.apiUrl ?? 'https://api.example.com',
  apiKey: Constants.expoConfig?.extra?.apiKey ?? '',
  environment: Constants.expoConfig?.extra?.environment ?? 'development',
  isDev: __DEV__,
  isProd: !__DEV__,
};

Using React Native CLI

# Install react-native-config
npm install react-native-config
cd ios && pod install
# .env
API_URL=https://api.example.com
API_KEY=your-api-key

# .env.staging
API_URL=https://staging-api.example.com
API_KEY=staging-api-key

# .env.production
API_URL=https://api.example.com
API_KEY=production-api-key
// src/shared/constants/config.ts
import Config from 'react-native-config';

export const config = {
  apiUrl: Config.API_URL ?? 'https://api.example.com',
  apiKey: Config.API_KEY ?? '',
  isDev: __DEV__,
  isProd: !__DEV__,
};

Environment-Specific Builds

// package.json
{
  "scripts": {
    "start": "expo start",
    "start:staging": "ENVFILE=.env.staging expo start",
    "start:prod": "ENVFILE=.env.production expo start",
    "build:staging": "eas build --profile staging",
    "build:prod": "eas build --profile production"
  }
}

Configuration Files

ESLint Configuration

// .eslintrc.js
module.exports = {
  root: true,
  extends: [
    'expo',
    '@react-native',
    'plugin:@typescript-eslint/recommended',
    'plugin:react-hooks/recommended',
    'prettier',
  ],
  plugins: ['@typescript-eslint', 'react-hooks', 'import'],
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaFeatures: {
      jsx: true,
    },
    ecmaVersion: 2021,
    sourceType: 'module',
  },
  rules: {
    // TypeScript
    '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
    '@typescript-eslint/explicit-function-return-type': 'off',
    '@typescript-eslint/no-explicit-any': 'warn',
    
    // React
    'react/react-in-jsx-scope': 'off',
    'react-hooks/rules-of-hooks': 'error',
    'react-hooks/exhaustive-deps': 'warn',
    
    // Import
    'import/order': [
      'error',
      {
        groups: [
          'builtin',
          'external',
          'internal',
          ['parent', 'sibling'],
          'index',
        ],
        'newlines-between': 'always',
        alphabetize: { order: 'asc' },
      },
    ],
    'import/no-duplicates': 'error',
  },
  settings: {
    'import/resolver': {
      typescript: {
        alwaysTryTypes: true,
      },
    },
  },
};

Prettier Configuration

// .prettierrc
{
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "es5",
  "printWidth": 80,
  "bracketSpacing": true,
  "arrowParens": "avoid",
  "endOfLine": "lf"
}

TypeScript Configuration

// tsconfig.json
{
  "extends": "expo/tsconfig.base",
  "compilerOptions": {
    "strict": true,
    "noEmit": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "react-native",
    "moduleResolution": "bundler",
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  },
  "include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"],
  "exclude": ["node_modules", "babel.config.js", "metro.config.js"]
}

Git Configuration

.gitignore

# Dependencies
node_modules/
.pnp/
.pnp.js

# Expo
.expo/
dist/
web-build/

# Native builds
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
*.orig.*

# iOS
ios/Pods/
ios/build/
ios/*.xcworkspace/xcuserdata/

# Android
android/.gradle/
android/app/build/
android/build/
*.apk
*.aab

# Metro
.metro-health-check*

# Testing
coverage/

# Environment
.env
.env.*
!.env.example

# IDE
.idea/
.vscode/
*.swp
*.swo
.DS_Store

# Debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Misc
*.log
.cache/

.gitattributes

# Auto detect text files and perform LF normalization
* text=auto

# JS/TS files
*.js text eol=lf
*.jsx text eol=lf
*.ts text eol=lf
*.tsx text eol=lf
*.json text eol=lf

# Config files
*.yml text eol=lf
*.yaml text eol=lf
*.md text eol=lf

# Shell scripts
*.sh text eol=lf

# Windows batch files
*.bat text eol=crlf
*.cmd text eol=crlf

# Binary files
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.ttf binary
*.woff binary
*.woff2 binary

Naming Conventions

Files and Folders

✅ Good naming:
src/
├── features/
│   └── user-profile/           # kebab-case for folders
│       ├── UserProfile.tsx     # PascalCase for components
│       ├── useUserProfile.ts   # camelCase with 'use' prefix for hooks
│       ├── userProfile.types.ts # camelCase with .types suffix
│       └── userProfile.utils.ts # camelCase with .utils suffix

❌ Bad naming:
src/
├── Features/                   # Don't use PascalCase for folders
│   └── UserProfile/
│       ├── user-profile.tsx    # Don't use kebab-case for components
│       ├── UserProfileHook.ts  # Don't suffix hooks with 'Hook'

Component Naming

// ✅ Good
export function ProductCard() { }
export function UserAvatar() { }
export function NavigationHeader() { }

// ❌ Bad
export function productCard() { }  // Should be PascalCase
export function Product_Card() { } // No underscores
export function ProductCardComponent() { } // Don't suffix with 'Component'

Hook Naming

// ✅ Good
export function useAuth() { }
export function useProducts() { }
export function useLocalStorage() { }

// ❌ Bad
export function Auth() { }        // Must start with 'use'
export function getProducts() { } // Must start with 'use'
export function useAuthHook() { } // Don't suffix with 'Hook'

Type Naming

// ✅ Good
interface User { }
interface ProductCardProps { }
type AuthState = { }
type ProductCategory = 'electronics' | 'clothing';

// ❌ Bad
interface IUser { }              // Don't prefix with 'I'
interface UserInterface { }      // Don't suffix with 'Interface'
type TAuthState = { }           // Don't prefix with 'T'

Quick Reference

Project Structure Checklist

1

Create Feature Folders

Organize code by feature, not by type (components, screens, etc.)
2

Set Up Path Aliases

Configure @/ aliases in tsconfig.json and babel.config.js
3

Create Barrel Exports

Add index.ts files to export public APIs from each module
4

Configure Environment Variables

Set up .env files and config constants
5

Add Linting and Formatting

Configure ESLint and Prettier for consistent code style

Mobile Project Structure Pitfalls

Structural mistakes that compound over time:Putting platform-specific code in shared directories. If shared/components/DatePicker.tsx contains Platform.OS === 'ios' ? ... branches that grow to 200+ lines, split it into DatePicker.ios.tsx and DatePicker.android.tsx. The file-extension convention keeps Metro happy and makes each platform’s code independently readable.Circular barrel exports. When features/auth/index.ts re-exports from features/auth/hooks/useAuth.ts, and that hook imports from features/products/index.ts, which imports from features/auth/index.ts — Metro’s module resolver can silently return undefined for the circular import. You will get a cryptic “X is not a function” runtime error with no useful stack trace. Break cycles by importing from the specific file, not the barrel, when a cross-feature dependency exists.Ignoring the ios/ and android/ native directories. These are not “generated files you never touch.” When you run npx pod-install or update Gradle plugin versions, files in these directories change. Commit them to version control. If a teammate’s ios/Pods/ is out of sync with yours, you will waste hours debugging build failures that have nothing to do with your JavaScript code.Storing secrets in app.json or .env files that get bundled. React Native bundles are just ZIP files containing JavaScript. Anyone can extract and read them. Use your backend as a proxy for secret API keys, and use Expo’s expo-secure-store or the native Keychain/Keystore for tokens that must live on-device.

Next Steps

Module 4: TypeScript in React Native

Learn TypeScript patterns and best practices for React Native development