Skip to main content
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. 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:
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:
// 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

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

Next Steps

Module 4: TypeScript in React Native

Learn TypeScript patterns and best practices for React Native development