DEV Community

Cover image for How Instagram, WhatsApp, Uber & Netflix Would Be Built Today Using Expo Router
SATYA SOOTAR
SATYA SOOTAR

Posted on

How Instagram, WhatsApp, Uber & Netflix Would Be Built Today Using Expo Router

When we think about apps like Instagram, WhatsApp, Uber, or Netflix, it's easy to get lost in the glamour of their features. The smooth feed, the instant message delivery, the live driver tracking, the seamless streaming. But what truly separates these giants from the millions of apps that never make it off the ground isn't just a clever idea, it's the architecture that holds it all up.

This isn't a blod on how to clone their UI pixel by pixel. You can find plenty of those. Instead, we're going to put on our engineering hats and explore how you would think about building the core systems of these apps from the ground up, using modern tools like Expo Router. The goal is to shift your mindset from "getting it to work" to "engineering for the long haul" and to show you how the right architecture makes all the difference.

The Myth of the Simple Folder Structure

We've all been there. You start a new project, a brilliant idea in your head, and the folder structure looks something like this:

src/
├── components/
├── screens/
├── hooks/
├── utils/
└── types/
Enter fullscreen mode Exit fullscreen mode

This feels clean and organized. For a small app with five screens, it works perfectly. But then, a year later, your startup has taken off, the team has grown from 1 to 15 engineers, and the app now has 50 screens.

This simple folder structure becomes a nightmare. Opening the components/ folder is like looking into a junk drawer. You see Button.tsx, FeedPost.tsx, ChatBubble.tsx, RideCard.tsx, and no one knows which components are used where. Deleting a feature becomes a game of "hunt the import" across the entire project. It slows everyone down, and worse, it makes refactoring terrifying.

For a small app, this structure is fine. For a production-scale app that demands high performance, maintainability, and team collaboration, it's an absolute disaster. The simple way fails because it's fundamentally based on the "type" of a file, not the "function" of a feature. This is the first, and perhaps most important, lesson of scaling your React Native application.

A Better Way: The Feature-Based Architecture

The production way of thinking is to organize your code by features, not by file types. It's called "feature-based separation" or "colocation". The idea is simple: all the code that makes a single feature work lives in its own cozy folder.

Let's visualize the difference. Imagine an app with a feed and a user profile.

Small App vs. Production App Architecture

In the production structure, if you want to delete the "feed" feature, you delete the /feed folder. Done. No hunting around. It's a massive cognitive win for developers, especially when a team is working on different features simultaneously.

Expo Router: The Architect's Best Friend

This is where Expo Router shines. At its core, Expo Router is a file-based routing library built on top of React Navigation. This means the file structure of your app folder directly defines your app's navigation. It's incredibly powerful for large applications because it enforces a clear, predictable navigation pattern that scales without any additional mental overhead.

Let's see how we can marry feature-based architecture with Expo Router's file system.

app/
  _layout.tsx            // Root layout (check auth, setup providers)
  (auth)/               // Auth route group, no tab bar
    _layout.tsx
    login.tsx
    sign-up.tsx
  (app)/                // Protected app route group with tabs
    _layout.tsx         // Tabs layout (Feed, Search, Activity, Profile)
    (feed)/
      _layout.tsx       // Stack navigator for Feed
      index.tsx
      [postId].tsx
    (messages)/
      _layout.tsx       // Stack for Chat
      index.tsx
      [chatId].tsx
    profile/
      _layout.tsx
      index.tsx
      settings.tsx
Enter fullscreen mode Exit fullscreen mode

This structure is the foundation. It tells us instantly what's public (auth), what's protected (app), and how the user moves through the app.

Building the Core Systems: A Four-Part Deep Dive

Now, let's use this architecture to think about the core systems for our four apps. We're not building a clone, but rather, understanding the architectural decisions behind their core features.

1. Instagram: The Infinite Feed

Instagram's main challenge is handling an endless stream of media-rich content. The core technical requirement is a high-performance, lazy-loading infinite scroll.

Expo Router Structure:
The (feed) folder would contain the main feed screen (index.tsx) and the dynamic route for a single post ([postId].tsx). This allows for deep linking directly to any post.

State Management: A combination of Zustand for global UI state (like the current user's feed preferences) and TanStack Query (formerly React Query) for managing server-state is a modern, powerful choice. TanStack Query is a master at caching, background refetching, and pagination, which is exactly what a feed needs.

Architecture in Action:
The Feed screen would use a FlatList with getItemLayout for memory efficiency. As the user scrolls, TanStack Query's useInfiniteQuery hook is triggered, fetching the next page of posts from an API. The API returns the data, which is then cached, and the UI updates instantly.

Feed rendering and infinite scroll flow

The Production Tradeoff: To keep the app fast and prevent it from consuming too much memory, you implement virtualized lists and heavy pagination. The tradeoff is complexity in data fetching and state synchronization, but it's the only way to handle a feed that could literally go on forever.

2. WhatsApp: Real-time Messaging with Offline-First Support

WhatsApp is a masterclass in real-time communication and offline resilience. You type a message to a friend in a different country, and they see the little typing indicator before the message pops into their chat, instantly.

Expo Router Structure:
The chat interface would live in the (messages)/[chatId].tsx screen. The deep linking with [chatId] is crucial. When a user taps a push notification for a new message, Expo Router can automatically navigate them directly to that specific chat using its built-in universal linking.

Core Technologies:

  • WebSockets (e.g., Socket.IO): For the real-time, persistent connection to send and receive messages instantly.
  • Offline-First Database (e.g., SQLite, WatermelonDB): All messages are first saved to a local database on the user's phone before they're even sent to the server.
  • Optimistic Updates: The UI updates instantly. Your message appears in the chat bubble before the network request is complete.

Architecture in Action:

  1. You Send a Message: The UI calls a function. The message is saved to the local SQLite database and given a temporary id. The UI instantly renders the new message with a "sending" status icon.
  2. Background Sync: A background job picks up the unsent message from the queue. It sends it to the server via the WebSocket connection.
  3. Server Confirms: The server receives the message, broadcasts it to the recipient, and sends a confirmation back to your device.
  4. Update Local State: Your app receives the confirmation, updates the local database, and changes the message status from "sending" to "sent".

Offline first message sync flow diagram

The Production Tradeoff: The complexity here is high. You have to manage optimistic UI updates, conflict resolution (what if the message fails to send?), and keep your local database perfectly in sync with the server's version. The benefit, a blazing fast and reliable messaging experience that works on a shaky train connection, is worth every bit of the effort.

3. Uber: Maps and Live Location Tracking

Uber's magic is the live map. You see a car moving in real-time, and its ETA updating by the second. This is all about managing a constant stream of location data.

Expo Router Structure:
The ride-tracking screen is a huge part of the app's flow. The (app) group for a logged-in user would contain a ride folder with dynamic routes for an active ride, like ride/[rideId].tsx.

Core Technologies:

  • Maps SDK (e.g., MapView, Google Maps API): For rendering the map and driver's location.
  • WebSocket for Location Streaming: The driver's phone is constantly publishing its coordinates to a server. The server then broadcasts this to the rider's app via a WebSocket connection.
  • Efficient Re-rendering: Every new coordinate cannot cause a full screen re-render. You need to update only the map's marker.

Architecture in Action:

  1. Ride Starts: The backend creates a unique room for the ride.
  2. Driver Publishes Location: Every few seconds, the driver's app sends a "location update" message to the server's room.
  3. Rider Listens: The rider's app is subscribed to the same room via a WebSocket.
  4. Map Updates: When a new location arrives, the app uses a reference to the map's marker to smoothly animate it to the new position, without redrawing the whole screen.

Real-time Driver Tracking

The Production Tradeoff: The biggest challenge is handling spotty connections. What happens when the driver enters a tunnel? The app needs a robust reconnection strategy with exponential backoff and smart UI states that don't make the rider think the driver has vanished into thin air. This adds significant complexity to the WebSocket management.

4. Netflix: Heavy Content Delivery

Netflix's challenge is getting you from a giant list of movies to watching one, all with zero buffering. It's a masterclass in loading states, smart pre-fetching, and efficient video playback.

Expo Router Structure:
The sheer number of screens is a perfect use case for Expo Router's dynamic routes. You'd have a movie/[movieId].tsx route that can deep-link to any piece of content in Netflix's massive library.

Core Technologies:

  • FlatList with Horizontal Scrolling: For rendering rows of movie posters, optimized for performance.
  • Thumbnail Pre-fetching: As the user scrolls, the app starts downloading the images for the next set of movies before they're even on screen.
  • Lazy Loading: The video player and its heavy dependencies are only loaded when the user navigates to the movie screen.
  • Offline Downloads: Users can download content directly to the device's file system for later viewing.

Architecture in Action:

  1. Browse: The home screen uses FlatList to render rows. Each row is a horizontal list of movie thumbnails.
  2. Prefetch: As the user stops scrolling, the app examines what's about to come into view and sends a low-priority request to download those images.
  3. Select a Movie: The user taps on "Stranger Things".
  4. Lazy Load Details Screen: The app quickly navigates to the movie/[movieId].tsx screen. This screen's code bundle is small and loads fast.
  5. Defer Loading: Immediately, the screen shows a skeleton UI and starts fetching movie details. The actual video player component, which is huge, is only imported and rendered after the details have loaded, keeping the initial navigation snappy.

App Startup Optimization Lifecycle

The Production Tradeoff: You trade a super simple "load everything at once" model for a much more complex "load on demand" system. Managing the state of what is loading, what is pre-fetched, and what is cached adds layers of complexity, but it's the only way to ensure your app doesn't feel slow and bloated.

Putting It All Together: The Production-Grade Starter

So, what does all of this look like in a single, coherent architecture? Here's a diagram of a production-grade folder structure using the principles we've discussed.

Production-Grade Expo Router Folder Structure

This structure is your roadmap. It's how you build an app that doesn't just work for a few months, but for years.

Navigating the Waters: Authentication Flows and Shared Layouts

Two of the most common challenges in any app are managing the authentication flow and creating nested, shared layouts. Expo Router handles both beautifully.

Authentication Flow: With route groups, your (auth) and (app) folders are completely separate. Inside the root _layout.tsx, you would check an authentication state (from your Zustand store or a secure token). If the user is logged out, it renders the (auth) group (login, sign-up). If they're logged in, it renders the (app) group (the main tabs). Expo Router v5 introduced Stack.Protected which makes this pattern even more declarative and secure.

Shared Layouts and Nested Routing: You can easily create a stack navigator inside a tab. For instance, your feed screen can be a stack. The main (app)/_layout.tsx creates the tabs. Then, inside the (feed)/_layout.tsx, you create a stack navigator. This allows you to push a post details screen ([postId].tsx) on top of the feed, while the bottom tab bar remains visible.

The Unseen Hero: The API and Networking Layer

All the data for your app comes from an API. A clean networking layer is crucial. You should never directly call fetch from within a screen component. Instead, you create a service layer.

You would have a services/api.ts file that configures a single instance of an HTTP client (like Axios). It sets up base URLs, authentication headers, and response interceptors for error handling. Then, for each feature, you create a feature-specific API file (e.g., feed/api.ts) that calls your base client and exposes simple functions like fetchFeed(). Your UI components only call these simple functions, keeping them blissfully unaware of the underlying network implementation.

Wrapping Up: It's About Engineering Mindset

Building an app at the scale of Instagram, WhatsApp, Uber, or Netflix isn't about a single magic library or a clever piece of code. It's a million small, deliberate decisions about architecture, tradeoffs, and developer experience.

The tools we have today, especially Expo Router, provide an incredible foundation. With its file-based routing, deep linking, and universal design, it forces you into a structure that scales naturally. But a tool is only as good as the person wielding it.

The next time you open a new React Native project, resist the temptation of the simple folder structure. Take a moment to think about the future. Map out your features. Group your routes. Ask yourself, "How would this code look to a new developer a year from now?".

By asking these questions and following the principles of feature-based architecture, you're not just building an app. You're engineering a system built to last.


Hope you liked this blog. If there’s any mistake or something I can improve, do tell me. You can find me on LinkedIn and X, I post more stuff there.

Top comments (0)