Over the years, one lesson has repeated itself across teams and products:
Your folder structure is not about aesthetics — it’s about decision-making at scale.
Recently, I’ve been working on a React Native app using Expo + Expo Router, and I want to share a structure that has worked exceptionally well for large, long-lived apps.
This post is meant to share insights with the dev community, especially folks building apps that go beyond MVPs.
Note: All the below folders will reside in src/ folder at the highest level.
src/
└── 📁src
└── 📁app
└── 📁components
└── 📁config
└── 📁hooks
└── 📁lib
└── 📁providers
└── 📁screens
└── 📁utils
🧭 app/ — Routing as a First-Class Citizen
Expo Router shines when routes reflect user flow, not technical shortcuts.
└── 📁app
└── 📁(authenticated)
└── 📁(home-tabs)
└── 📁(unauthenticated)
├── _layout.tsx
└── index.tsx
Why this works so well:
(unauthenticated)
- Login, OTP, onboarding
- No tabs, no distractions
- Clear boundary for auth guards
(authenticated)
- Entry point after login
- Handles app-level layouts, redirects, and global state
(home-tabs)
- Only the screens that truly belong to bottom tabs
- Everything else (modals, flows, detail screens) lives outside tabs
This makes the flow crystal clear:
Unauthenticated → Authenticated → Tab-based home → Non-tab flows
No guessing. No accidental tab nesting. No router spaghetti.
🧱 components/ — Design System, Not Random Reuse
The structure follows Atomic Design, but applied pragmatically:
components/
├── atoms
├── molecules
├── organisms
└── templates
Key principles:
Atoms → Pure, reusable, testable UI primitives
Molecules → Small compositions with intent
Organisms → Feature-aware UI blocks
Templates → Layout patterns, not screens
This ensures:
- UI consistency across the app
- Easy refactors when design systems evolve
Components stay reusable without becoming generic junk drawers.
🧠 lib/ — The App’s Brain (Not a Dumping Ground)
Here, lib/ is intentionally structured:
lib/
├── auth
├── backend
├── implementation
├── interface
└── vector-icon
backend/
- API clients (Axios / fetch wrappers)
- TanStack Query client setup
- Server-state hooks
- Backend data models
- interface / implementation
- Clear contracts
- Platform-agnostic abstractions
- Easy to mock, test, or replace later
Eg.
└── 📁backend
└── 📁_models
└── 📁server-state
└── 📁queries
├── useGetThoughtOfDayApi.ts
├── query-client.ts
└── 📁supabase
├── supabase-client.ts
├── supabase-safe-call.ts
└── 📁supabase-db
└── fetch-though-of-day.ts
auth/
- Auth state, providers, and boundaries live together
- No auth logic leaking into UI
This separation pays off when:
- APIs change
- You swap backend providers
- You test without the network
📱 screens/ — Screens Are Not Routes
A subtle but important distinction.
screens/
├── authenticated
└── unauthenticated
- Screens contain UI + screen-level state
- Routes (app/) only decide when a screen is shown
- This keeps navigation thin and screens testable
Result:
Screens are portable. Routes are declarative.
🧰 utils/, hooks/, providers/ — Supporting the Scale
utils/
- Pure logic, zero React dependency
- Easy to test, easy to trust
hooks/
- App-specific behavior
- Not generic utilities disguised as hooks
providers/
- Theme, query client, safe area, global app context
- Single source of truth for app-wide concerns
🏗️ Why This Structure Scales
- Scales across multiple teams
- Encourages clear ownership
- Reduces cognitive load for new engineers
- Supports feature-based growth without rewrites
- Works equally well for React Native + Web (Expo)
Most importantly, it reflects how users move through the app, not how the framework works internally.
💡 Final Thought
- Frameworks evolve.
- Product requirements change.
- Teams grow.
A good folder structure doesn’t fight that — it absorbs it. Hope this helps someone designing their next large-scale React Native app. Would love to hear how others are structuring their Expo Router projects 👋
Hope this helps anyone designing a production-grade Expo Router app.
Would love to hear how others are structuring their apps 👇