Designing a Scalable React Native + Expo Router Folder Structure
Source: Dev.to
Folder Overview
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
(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
Flow: 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, applied pragmatically:
components/
├─ atoms
├─ molecules
├─ organisms
└─ templates
- Atoms – pure, reusable, testable UI primitives
- Molecules – small compositions with intent
- Organisms – feature‑aware UI blocks
- Templates – layout patterns (not full screens)
Benefits
- UI consistency across the app
- Easy refactors when the design system evolves
- Components stay reusable without becoming generic junk drawers
lib/ — The App’s Brain (Not a Dumping Ground)
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
- Clear contracts (interface / implementation)
- Platform‑agnostic abstractions – easy to mock, test, or replace
Example
// lib/backend/_models/...
// lib/backend/server-state/queries/useGetThoughtOfDayApi.ts
// lib/backend/server-state/query-client.ts
// lib/backend/supabase/supabase-client.ts
// lib/backend/supabase/supabase-safe-call.ts
// lib/backend/supabase-db/fetch-thought-of-day.ts
auth/
- Auth state, providers, and boundaries live together
- No auth logic leaks into UI
This separation pays off when APIs change, you swap backend providers, or you need to test without a network.
screens/ — Screens Are Not Routes
screens/
├─ authenticated
└─ unauthenticated
- Screens contain UI + screen‑level state.
- Routes (
app/) only decide when a screen is shown.
Result: Screens are portable; routes are declarative.
utils/, hooks/, providers/ — Supporting the Scale
| Directory | Purpose |
|---|---|
| utils | Pure logic, zero React dependency – easy to test and 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
- Works across multiple teams – clear ownership.
- Reduces cognitive load for new engineers.
- Supports feature‑based growth without rewrites.
- Works equally well for React Native and Web (Expo).
Most importantly, it mirrors how users move through the app, not how the framework is organized internally.
Final Thought
- Frameworks evolve.
- Product requirements change.
- Teams grow.
A good folder structure doesn’t fight that – it absorbs it.
Pro Tip
Keep screen components lean. Let screens focus on composition and navigation, while individual section components own their state, custom hooks, and API/TanStack Query logic close to where it’s used.
Hope this helps anyone designing a production‑grade Expo Router app. Would love to hear how others are structuring their projects!