Look, I've spent the last few months building e-commerce apps, and honestly? Most tutorials out there are trash. They show you how to display products in a FlatList and call it a day. That's not how real apps work.
So I'm documenting my complete approach to building a proper e-commerce application that's actually ready for the App Store and Google Play. No shortcuts, no "we'll add this later" nonsense. Just pure, production-grade architecture.
Why Another E-Commerce Tutorial?
Because most of them suck. They don't talk about:
- How to actually structure your codebase so you're not drowning in spaghetti code by week 2
- Real state management that doesn't make you want to cry
- Proper navigation patterns that users actually understand
- Performance optimization (because nobody wants a laggy checkout)
- How to handle offline mode (yes, people still lose internet connection)
This isn't a "follow along" tutorial. This is the architecture document I wish I had when I started.
The Tech Stack (and Why)
React Native with Expo - Hot take: Expo is no longer the "beginner framework." With EAS and custom dev clients, you get 90% of the benefits with 10% of the headache. Unless you're building a crypto wallet or some AR-heavy app, Expo is the move.
TypeScript - If you're still writing JavaScript in 2025, we need to talk. Type safety isn't optional anymore.
Zustand for State Management - Redux is overkill. Context API is underwhelming. Zustand is the Goldilocks solution. Simple, performant, and you can actually understand it after 3 months away from the code.
React Navigation - Because, well, there's no real alternative.
TanStack Query - Server state is NOT the same as client state. Stop putting API data in your global store. React Query handles caching, refetching, and background updates like a boss.
Stripe for Payments - Because rolling your own payment system is how you end up on the news.
File Structure That Won't Make You Hate Yourself
Here's the folder structure I'm using. It's opinionated, but it scales:
/src
/api
/services
authService.ts
productService.ts
orderService.ts
/hooks
useProducts.ts
useCart.ts
useAuth.ts
client.ts
/components
/common
Button.tsx
Input.tsx
Card.tsx
LoadingSpinner.tsx
/product
ProductCard.tsx
ProductGrid.tsx
ProductDetail.tsx
/cart
CartItem.tsx
CartSummary.tsx
/checkout
CheckoutForm.tsx
PaymentSheet.tsx
/screens
/auth
LoginScreen.tsx
RegisterScreen.tsx
/home
HomeScreen.tsx
/product
ProductListScreen.tsx
ProductDetailScreen.tsx
/cart
CartScreen.tsx
/checkout
CheckoutScreen.tsx
/profile
ProfileScreen.tsx
OrderHistoryScreen.tsx
/navigation
RootNavigator.tsx
AuthNavigator.tsx
MainNavigator.tsx
types.ts
/store
useAuthStore.ts
useCartStore.ts
useThemeStore.ts
/utils
validation.ts
formatting.ts
constants.ts
/types
product.types.ts
user.types.ts
order.types.ts
/assets
/images
/fonts
/icons
/app.json
/babel.config.js
/tsconfig.json
The Key Principle: Feature-Based + Layer-Based Hybrid
Notice how I'm not going full feature-based (where everything for "products" lives in one folder) or full layer-based (where all components live together). It's a hybrid.
Why? Because in e-commerce, some things are truly shared (Button, Input), while others are feature-specific (ProductCard). This structure respects both realities.
System Architecture: The Real Stuff
Here's how the data flows:
Authentication Layer
User Input → useAuthStore → authService → Backend
↓
Zustand Store (token)
↓
Persisted Storage
↓
API Client (auto-inject token)
Every API call automatically includes the auth token. If a 401 comes back, we auto-logout. No manual token management in components.
Product Discovery Flow
HomeScreen → useProducts hook → TanStack Query
↓
productService.ts
↓
API Client → Backend
↓
Cached in React Query
↓
Background Refetch (stale-while-revalidate)
Products are cached aggressively. Stale data is served instantly, fresh data loads in the background. Users never wait.
Cart Management
User Action → useCartStore (Zustand)
↓
Optimistic Update (instant UI)
↓
Async Sync to Backend
↓
Rollback on Failure
The cart feels instant because we're optimistic. Add to cart? It's there immediately. Backend call fails? We roll it back with a toast notification.
Checkout Flow
CartScreen → CheckoutScreen → Payment Intent
↓
Stripe SDK
↓
Confirmation
↓
Order Creation
↓
Clear Cart
↓
Order History
Nothing enters the order history until payment succeeds. Cart clearing is an atomic operation. No weird states.
The UI/UX Philosophy
I'm going for what I call "Invisible Excellence." The UI should be so intuitive that users never think about it.
Core Design Principles:
1. Minimalist Product Cards
No borders, no shadows, no nonsense. Just a great product photo, title, price. Let the products speak.
2. Bottom Sheet Everything
Filters? Bottom sheet. Product options? Bottom sheet. Users expect this pattern now. Don't fight it.
3. Gesture-First Navigation
Swipe to go back. Pull to refresh. Long press for quick actions. Make it feel native.
4. Skeleton Screens > Spinners
Loading states should look like the content they're replacing. Spinners are so 2018.
5. Micro-Interactions Matter
Button press animations. Cart icon bounce when adding items. Subtle haptic feedback. These details separate good apps from great apps.
Performance: The Stuff Nobody Talks About
Image Optimization
// Bad
<Image source={{ uri: product.image }} />
// Good
<Image
source={{ uri: product.image }}
placeholder={blurhash}
contentFit="cover"
transition={200}
cachePolicy="memory-disk"
/>
Using Expo Image with blurhash placeholders. Images load progressively, cache aggressively, and never cause jank.
List Performance
<FlashList
data={products}
estimatedItemSize={200}
renderItem={({ item }) => <ProductCard product={item} />}
removeClippedSubviews
maxToRenderPerBatch={10}
/>
FlashList over FlatList. 60fps scrolling is non-negotiable.
Code Splitting
const CheckoutScreen = lazy(() => import('./screens/checkout/CheckoutScreen'));
Payment SDK is 2MB. Don't load it until checkout. Bundle size matters.
State Management Strategy
Here's the controversial part: I use THREE state management solutions, and it's perfect.
Zustand: Client state (cart, auth, preferences)
TanStack Query: Server state (products, orders)
React Context: Theme and i18n only
Each tool does what it's best at. Stop trying to make one solution handle everything.
Offline Support
E-commerce apps need to work offline. At least partially.
// Cached product browsing
const { data: products } = useProducts({
staleTime: 1000 * 60 * 60, // 1 hour
cacheTime: 1000 * 60 * 60 * 24, // 24 hours
});
// Queue cart updates
const addToCart = useCartStore(state => state.addItem);
// Automatically syncs when connection restores
Users can browse previously viewed products offline. Cart changes queue up and sync when online. Orders require connection (obviously).
Navigation Architecture
// Stack-based with modal support
Root Navigator (Stack)
├── Auth Navigator (Stack)
│ ├── Login
│ └── Register
└── Main Navigator (Tabs)
├── Home (Stack)
├── Search (Stack)
├── Cart (Stack)
└── Profile (Stack)
Deep linking is configured for every screen. Push notifications? They work. Product share links? They open the right screen. Universal links? Handled.
Security Checklist
- ✅ Tokens in secure storage (not AsyncStorage)
- ✅ Certificate pinning for API calls
- ✅ No sensitive data in Redux DevTools
- ✅ Payment data never touches our servers
- ✅ Biometric authentication for checkout
- ✅ Session timeout on background
Testing Strategy
Unit Tests: Pure functions and utilities
Integration Tests: API services with MSW
E2E Tests: Critical paths with Detox (login, purchase)
I don't test UI components. Fight me. They change too fast, and the ROI isn't there.
Deployment Pipeline
Git Push → GitHub Actions
↓
Run Tests
↓
EAS Build (iOS + Android)
↓
Upload to TestFlight + Play Internal
↓
Automated E2E Tests
↓
Staging Deployment
↓
Manual Approval
↓
Production Release
One command. Fifteen minutes later, it's in TestFlight. Zero manual steps.
The Mistakes I Made (So You Don't Have To)
1. Started without TypeScript
Added it later. That migration took 3 weeks. Just start with it.
2. Put everything in Redux
Loading states, API data, user preferences, cart, all in one store. Debugging was hell.
3. Ignored accessibility
Submitted to App Store. Got rejected. Added proper labels. Submitted again. Approved.
4. Over-engineered early
Built a complex theming system before having a single screen working. YAGNI is real.
5. Didn't set up error tracking immediately
Production bugs happened. Had no idea what users were experiencing. Added Sentry day one now.
What's Next?
This architecture is just the foundation. Here's what comes next:
- Push notifications for order updates
- Wishlists with sync across devices
- Product recommendations (actual ML, not random)
- AR try-on for applicable products
- Social sharing with dynamic OG images
- Subscription model for recurring products
The Reality Check
Building a production e-commerce app isn't a weekend project. This architecture took months to refine. But once it's set up? Adding new features is fast. Onboarding developers is easy. The app is stable.
Most importantly: users don't complain. They just buy stuff and leave good reviews.
That's the goal.
Resources That Actually Helped
- React Native EU talks (skip the intro ones)
- Shopify's mobile engineering blog
- Stripe's React Native SDK docs
- Expo's EAS documentation
- TanStack Query docs (seriously, read them)
No courses. No paid content. Just official docs and conference talks from people building real apps.
Final Thoughts
You don't need a perfect architecture to start. But you need a plan. This is mine. Take what works, ignore what doesn't, adapt to your needs.
The best code is code that ships. But shipped code that's maintainable? That's the dream.
Now stop reading and start building. The App Store isn't going to submit to itself.
That's a wrap 🎁
Now go touch some code 👨💻
Top comments (0)