Designing a Scalable React Native + Expo Router Folder Structure

Published: (February 8, 2026 at 06:03 AM EST)
3 min read
Source: Dev.to

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

DirectoryPurpose
utilsPure logic, zero React dependency – easy to test and trust.
hooksApp‑specific behavior – not generic utilities disguised as hooks.
providersTheme, 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!

0 views
Back to Blog

Related posts

Read more »

Good rule for Expo Vs RN

A good rule: If you can go 3–6 months without touching Xcode/Gradle, Expo is almost always the better trade. If you’ll touch native every sprint, go bare....