As a backend developer who has spent years working with Node.js, Express, databases, and APIs, stepping into mobile development felt like learning to drive on the other side of the road — the fundamentals are the same, but everything is mirrored. This article documents my journey into React Native with Expo, the challenges I faced, and the surprising ways backend experience transfers to mobile development.
Why React Native?
I chose React Native for a practical reason: I already know JavaScript and TypeScript. Learning Swift and Kotlin simultaneously while building a production app was not realistic. React Native gave me:
- Single codebase for iOS and Android.
- Expo as a managed workflow — no Xcode or Android Studio setup on day one.
- Hot reload for instant feedback during development.
- A familiar ecosystem — npm packages, TypeScript, and VS Code.
The Mental Model Shift
In backend development, you think in requests and responses. In mobile development, you think in screens and state.
| Backend | Mobile |
|---|---|
| Endpoints (routes) | Screens (navigation) |
| Middleware chain | Component lifecycle |
| Database queries | API calls + local cache |
| Server memory | Device memory (limited!) |
| Request/response | User interactions |
The biggest adjustment: everything is asynchronous and visual. A database query returns data silently; a mobile screen must show loading states, error states, empty states, and success states — all gracefully.
Getting Started with Expo
Expo abstracts away the native build toolchain. Setting up a new project takes one command:
npx create-expo-app@latest my-app --template blank-typescript
cd my-app
npx expo startScan the QR code with the Expo Go app on your phone, and you are developing on a real device in under 2 minutes.
Navigation with Expo Router
Expo Router brings file-based routing (like Next.js) to React Native. Create a file, and it becomes a screen:
app/
├── _layout.tsx # Root layout (Tab/Stack navigation)
├── index.tsx # Home screen (/)
├── profile.tsx # Profile screen (/profile)
└── (auth)/
├── login.tsx # Login screen (/login)
└── register.tsx # Register screen (/register)The layout file defines the navigation structure:
import { Tabs } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
export default function TabLayout() {
return (
<Tabs>
<Tabs.Screen
name="index"
options={{
title: 'Home',
tabBarIcon: ({ color }) => <Ionicons name="home" size={24} color={color} />,
}}
/>
<Tabs.Screen
name="profile"
options={{
title: 'Profile',
tabBarIcon: ({ color }) => <Ionicons name="person" size={24} color={color} />,
}}
/>
</Tabs>
);
}Where Backend Skills Shine
API Integration
This is where I felt most at home. Fetching data from a REST API in React Native is identical to fetching it in a React web app:
import { useEffect, useState } from 'react';
interface User {
id: string;
name: string;
email: string;
}
function useUser(userId: string) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function fetchUser() {
try {
const res = await fetch(`https://api.example.com/users/${userId}`);
if (!res.ok) throw new Error('Failed to fetch user');
const data = await res.json();
setUser(data);
} catch (err: any) {
setError(err.message);
} finally {
setLoading(false);
}
}
fetchUser();
}, [userId]);
return { user, loading, error };
}Secure Token Storage
My understanding of JWT and refresh tokens translated directly. The only difference is *where* you store them:
import * as SecureStore from 'expo-secure-store';
async function saveToken(token: string) {
await SecureStore.setItemAsync('accessToken', token);
}
async function getToken(): Promise<string | null> {
return SecureStore.getItemAsync('accessToken');
}Data Validation
Zod schemas work identically in React Native. I reuse the same validation schemas from my backend:
import { z } from 'zod';
const loginSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
});Challenges I Faced
1. Styling Without CSS
React Native uses a subset of CSS via StyleSheet. No cascading, no class names, no media queries out of the box:
import { StyleSheet } from 'react-native';
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16,
backgroundColor: '#fff',
},
title: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 8,
},
});2. Debugging Is Different
No browser DevTools. Instead, use React Native Debugger, Flipper, or Expo's built-in dev tools. Console.log still works, but inspecting network requests requires additional tooling.
3. Platform Differences
iOS and Android render components differently. Shadows, status bars, and safe areas need platform-specific handling:
import { Platform, StatusBar } from 'react-native';
const styles = StyleSheet.create({
safeArea: {
paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0,
},
});What I Wish I Knew Earlier
- Start with Expo, not bare React Native. The managed workflow removes 90 % of the setup pain.
- Use TypeScript from day one. The type safety catches bugs that are harder to debug on mobile.
- Design for offline. Mobile users lose connectivity. Plan for it with local caching and optimistic updates.
- Test on real devices early. The simulator hides performance issues and interaction quirks.
Key Takeaways
- Backend developers have a massive head start in React Native — API integration, authentication, validation, and data modeling all transfer directly.
- The learning curve is in UI composition, not logic — think in components, not endpoints.
- Expo makes the entry barrier remarkably low — you can prototype a full mobile app in a weekend.
- The combination of a strong backend and mobile skills makes you a genuinely full-stack developer.
My React Native journey is ongoing, and every week I find new ways my backend experience accelerates my mobile development. If you are a backend developer curious about mobile, take the leap — you will be surprised how much you already know.
