DEV Community

Sachin Rupani
Sachin Rupani

Posted on

Designing a Scalable React Native + Expo Router Folder Structure

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
Enter fullscreen mode Exit fullscreen mode

🧭 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • 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 👋

☑️ Pro tip:
Keep screen components lean. Let screens focus on composition and navigation, while individual section components own their state, custom hooks and API or 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 apps 👇

Top comments (0)