What are Mobile APIs?

APIs (Application Programming Interfaces) are how your mobile app communicates with backend servers to get or send data. Think of APIs like a waiter in a restaurant - you (the app) tell the waiter (API) what you want, the waiter goes to the kitchen (server), and brings back your food (data).

Unlike web apps that run on stable WiFi connections, mobile apps face unique challenges: spotty connections, slow networks, offline scenarios, and limited data plans. Mobile API patterns are specifically designed to handle these challenges gracefully.

Why Mobile APIs Are Different

  • Unreliable Networks: Users move between WiFi, 4G, 3G, or no connection
  • Slow Connections: Mobile networks slower than broadband
  • Data Costs: Users pay for mobile data usage
  • Battery Life: Network calls drain battery
  • Offline Usage: Apps should work without internet when possible
  • Background Restrictions: OS limits what apps can do in background
  • Variable Screen Sizes: Need to fetch appropriate data amounts

REST API Basics

REST (Representational State Transfer) is the most common API style. It uses standard HTTP methods to interact with resources.

HTTP Methods (CRUD Operations):
┌──────────────────────────────────────────┐
│ GET    - Read/Retrieve data              │
│ POST   - Create new data                 │
│ PUT    - Update/Replace entire resource  │
│ PATCH  - Update part of resource         │
│ DELETE - Remove data                     │
└──────────────────────────────────────────┘

REST API Endpoints Structure:
GET    /api/users           → Get all users
GET    /api/users/123       → Get user with ID 123
POST   /api/users           → Create new user
PUT    /api/users/123       → Update user 123
PATCH  /api/users/123       → Partial update user 123
DELETE /api/users/123       → Delete user 123

Response Status Codes:
200 - OK (success)
201 - Created (resource created)
204 - No Content (success but no data returned)
400 - Bad Request (invalid data sent)
401 - Unauthorized (need to login)
403 - Forbidden (logged in but no permission)
404 - Not Found (resource doesn't exist)
500 - Server Error (backend problem)

Making API Calls in React Native

Using Fetch API

// Basic GET Request
const fetchUsers = async () => {
  try {
    const response = await fetch('https://api.example.com/users');

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    const data = await response.json();
    console.log('Users:', data);
    return data;
  } catch (error) {
    console.error('Fetch failed:', error);
    throw error;
  }
};

// POST Request (Create)
const createUser = async (userData) => {
  try {
    const response = await fetch('https://api.example.com/users', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer YOUR_TOKEN_HERE'
      },
      body: JSON.stringify(userData)
    });

    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Create failed:', error);
    throw error;
  }
};

// PUT Request (Update)
const updateUser = async (userId, userData) => {
  const response = await fetch(`https://api.example.com/users/${userId}`, {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(userData)
  });
  return await response.json();
};

// DELETE Request
const deleteUser = async (userId) => {
  await fetch(`https://api.example.com/users/${userId}`, {
    method: 'DELETE'
  });
};

Using Axios (Better Alternative)

// Install: npm install axios
import axios from 'axios';

// Create configured instance
const api = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 10000, // 10 seconds
  headers: {
    'Content-Type': 'application/json'
  }
});

// Add auth token to all requests
api.interceptors.request.use((config) => {
  const token = getAuthToken(); // Get from storage
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// Handle errors globally
api.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      // Unauthorized - redirect to login
      navigateToLogin();
    }
    return Promise.reject(error);
  }
);

// GET
const fetchUsers = async () => {
  const response = await api.get('/users');
  return response.data;
};

// POST
const createUser = async (userData) => {
  const response = await api.post('/users', userData);
  return response.data;
};

// With query parameters
const searchUsers = async (query) => {
  const response = await api.get('/users', {
    params: { search: query, limit: 20 }
  });
  return response.data;
};

// Upload file
const uploadPhoto = async (file) => {
  const formData = new FormData();
  formData.append('photo', {
    uri: file.uri,
    type: 'image/jpeg',
    name: 'photo.jpg'
  });

  const response = await api.post('/upload', formData, {
    headers: { 'Content-Type': 'multipart/form-data' }
  });
  return response.data;
};

Complete Example with Loading & Error States

import React, { useState, useEffect } from 'react';
import { View, Text, FlatList, ActivityIndicator, Button } from 'react-native';
import axios from 'axios';

function UserListScreen() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [refreshing, setRefreshing] = useState(false);

  const fetchUsers = async () => {
    try {
      setError(null);
      const response = await axios.get('https://api.example.com/users');
      setUsers(response.data);
    } catch (err) {
      setError(err.message);
      console.error('Fetch failed:', err);
    } finally {
      setLoading(false);
      setRefreshing(false);
    }
  };

  useEffect(() => {
    fetchUsers();
  }, []);

  const handleRefresh = () => {
    setRefreshing(true);
    fetchUsers();
  };

  const handleRetry = () => {
    setLoading(true);
    fetchUsers();
  };

  // Loading state
  if (loading) {
    return (
      <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
        <ActivityIndicator size="large" />
        <Text>Loading users...</Text>
      </View>
    );
  }

  // Error state
  if (error) {
    return (
      <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
        <Text style={{ color: 'red' }}>Error: {error}</Text>
        <Button title="Retry" onPress={handleRetry} />
      </View>
    );
  }

  // Success state
  return (
    <FlatList
      data={users}
      keyExtractor={(item) => item.id.toString()}
      renderItem={({ item }) => (
        <View style={{ padding: 15, borderBottomWidth: 1 }}>
          <Text style={{ fontSize: 18 }}>{item.name}</Text>
          <Text style={{ color: '#666' }}>{item.email}</Text>
        </View>
      )}
      refreshing={refreshing}
      onRefresh={handleRefresh}
    />
  );
}

Making API Calls in Flutter

// Install: flutter pub add http
import 'package:http/http.dart' as http;
import 'dart:convert';

// GET Request
Future<List<User>> fetchUsers() async {
  final response = await http.get(
    Uri.parse('https://api.example.com/users'),
  );

  if (response.statusCode == 200) {
    List<dynamic> data = json.decode(response.body);
    return data.map((json) => User.fromJson(json)).toList();
  } else {
    throw Exception('Failed to load users');
  }
}

// POST Request
Future<User> createUser(User user) async {
  final response = await http.post(
    Uri.parse('https://api.example.com/users'),
    headers: {'Content-Type': 'application/json'},
    body: json.encode(user.toJson()),
  );

  if (response.statusCode == 201) {
    return User.fromJson(json.decode(response.body));
  } else {
    throw Exception('Failed to create user');
  }
}

// Using Dio (Better Alternative)
// flutter pub add dio
import 'package:dio/dio.dart';

class ApiService {
  final Dio _dio = Dio(
    BaseOptions(
      baseUrl: 'https://api.example.com',
      connectTimeout: Duration(seconds: 10),
      receiveTimeout: Duration(seconds: 10),
      headers: {'Content-Type': 'application/json'},
    ),
  );

  ApiService() {
    // Add interceptors
    _dio.interceptors.add(
      InterceptorsWrapper(
        onRequest: (options, handler) {
          // Add auth token
          final token = getToken();
          if (token != null) {
            options.headers['Authorization'] = 'Bearer $token';
          }
          return handler.next(options);
        },
        onError: (error, handler) {
          // Handle errors globally
          if (error.response?.statusCode == 401) {
            // Redirect to login
            navigateToLogin();
          }
          return handler.next(error);
        },
      ),
    );
  }

  Future<List<User>> getUsers() async {
    final response = await _dio.get('/users');
    return (response.data as List)
        .map((json) => User.fromJson(json))
        .toList();
  }

  Future<User> createUser(User user) async {
    final response = await _dio.post('/users', data: user.toJson());
    return User.fromJson(response.data);
  }
}

// Complete Widget Example
class UserListScreen extends StatefulWidget {
  @override
  _UserListScreenState createState() => _UserListScreenState();
}

class _UserListScreenState extends State<UserListScreen> {
  final ApiService _api = ApiService();
  List<User> users = [];
  bool isLoading = true;
  String? error;

  @override
  void initState() {
    super.initState();
    fetchUsers();
  }

  Future<void> fetchUsers() async {
    try {
      setState(() {
        isLoading = true;
        error = null;
      });

      final data = await _api.getUsers();

      setState(() {
        users = data;
        isLoading = false;
      });
    } catch (e) {
      setState(() {
        error = e.toString();
        isLoading = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    if (isLoading) {
      return Center(child: CircularProgressIndicator());
    }

    if (error != null) {
      return Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Error: $error'),
            ElevatedButton(
              onPressed: fetchUsers,
              child: Text('Retry'),
            ),
          ],
        ),
      );
    }

    return RefreshIndicator(
      onRefresh: fetchUsers,
      child: ListView.builder(
        itemCount: users.length,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text(users[index].name),
            subtitle: Text(users[index].email),
          );
        },
      ),
    );
  }
}

Offline-First Architecture

Offline-first means your app works even without internet. Data is stored locally and synced with server when connection is available. Think of it like a notebook - you can write notes anytime, and they're uploaded to cloud when you have WiFi.

Why Offline-First?

  • Better UX: App always works, no "No Internet" screens
  • Faster: Load from local storage, no waiting for network
  • Data Plans: Users save on mobile data
  • Reliability: Works in tunnels, planes, rural areas
  • Performance: Instant response, sync in background

Offline-First Pattern

Offline-First Flow:
┌────────────────────────────────────────┐
│ 1. App starts                          │
│    └─ Load data from local storage     │
│    └─ Display to user immediately      │
│                                        │
│ 2. Check internet connection           │
│    └─ If online: fetch from API        │
│    └─ Update local storage             │
│    └─ Update UI with fresh data        │
│                                        │
│ 3. User makes changes                  │
│    └─ Save to local storage first      │
│    └─ Update UI immediately            │
│    └─ Queue for sync                   │
│                                        │
│ 4. When online                         │
│    └─ Sync queued changes to server    │
│    └─ Resolve conflicts if any         │
└────────────────────────────────────────┘

Implementation with AsyncStorage

// Install: npm install @react-native-async-storage/async-storage
import AsyncStorage from '@react-native-async-storage/async-storage';
import NetInfo from '@react-native-community/netinfo';

// Offline-First Data Service
class DataService {
  static CACHE_KEY = '@app_data';

  // Load data (offline-first)
  static async loadUsers() {
    try {
      // 1. Load from cache immediately
      const cached = await AsyncStorage.getItem(this.CACHE_KEY);
      let users = cached ? JSON.parse(cached) : [];

      // 2. Check if online
      const netInfo = await NetInfo.fetch();

      if (netInfo.isConnected) {
        // 3. Fetch fresh data from API
        const response = await fetch('https://api.example.com/users');
        const freshData = await response.json();

        // 4. Update cache
        await AsyncStorage.setItem(this.CACHE_KEY, JSON.stringify(freshData));

        users = freshData;
      }

      return users;
    } catch (error) {
      console.error('Load failed:', error);
      // Return cached data even if API fails
      const cached = await AsyncStorage.getItem(this.CACHE_KEY);
      return cached ? JSON.parse(cached) : [];
    }
  }

  // Save data (offline-first)
  static async saveUser(user) {
    // 1. Save to local storage immediately
    const cached = await AsyncStorage.getItem(this.CACHE_KEY);
    const users = cached ? JSON.parse(cached) : [];
    users.push(user);
    await AsyncStorage.setItem(this.CACHE_KEY, JSON.stringify(users));

    // 2. Queue for sync
    await this.queueForSync('create_user', user);

    // 3. Try to sync if online
    await this.syncIfOnline();
  }

  // Queue changes for later sync
  static async queueForSync(action, data) {
    const queue = await AsyncStorage.getItem('@sync_queue');
    const items = queue ? JSON.parse(queue) : [];
    items.push({ action, data, timestamp: Date.now() });
    await AsyncStorage.setItem('@sync_queue', JSON.stringify(items));
  }

  // Sync queued changes
  static async syncIfOnline() {
    const netInfo = await NetInfo.fetch();
    if (!netInfo.isConnected) return;

    const queue = await AsyncStorage.getItem('@sync_queue');
    if (!queue) return;

    const items = JSON.parse(queue);

    for (const item of items) {
      try {
        if (item.action === 'create_user') {
          await fetch('https://api.example.com/users', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(item.data)
          });
        }
        // Remove from queue after successful sync
      } catch (error) {
        console.error('Sync failed:', error);
        break; // Stop syncing on error, try again later
      }
    }

    // Clear synced items
    await AsyncStorage.setItem('@sync_queue', JSON.stringify([]));
  }
}

// Usage in Component
function UserListScreen() {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    loadData();

    // Listen for network changes
    const unsubscribe = NetInfo.addEventListener(state => {
      if (state.isConnected) {
        DataService.syncIfOnline();
      }
    });

    return () => unsubscribe();
  }, []);

  const loadData = async () => {
    const data = await DataService.loadUsers();
    setUsers(data);
  };

  const addUser = async (userData) => {
    await DataService.saveUser(userData);
    loadData(); // Refresh UI
  };

  return (
    // UI code
  );
}

Caching Strategies

Caching stores data locally to avoid repeated network requests. Think of it like keeping frequently-used items on your desk instead of going to the filing cabinet every time.

Cache Types

1. Memory Cache (Fastest)
   - Stores in RAM
   - Lost when app closes
   - Use for: Current session data

2. Disk Cache (Persistent)
   - Stores on device storage
   - Survives app restarts
   - Use for: User data, images

3. HTTP Cache (Automatic)
   - Browser/HTTP library handles it
   - Based on Cache-Control headers
   - Use for: Static resources

4. CDN Cache (Server-side)
   - Content Delivery Network
   - Reduces server load
   - Use for: Images, videos, static files

Cache-First Strategy

// React Query - Automatic caching
// npm install @tanstack/react-query
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

function UserListScreen() {
  // Automatically caches and manages data
  const { data: users, isLoading, error } = useQuery({
    queryKey: ['users'],
    queryFn: async () => {
      const response = await fetch('https://api.example.com/users');
      return response.json();
    },
    staleTime: 5 * 60 * 1000, // Consider data fresh for 5 minutes
    cacheTime: 10 * 60 * 1000, // Keep in cache for 10 minutes
  });

  if (isLoading) return <ActivityIndicator />;
  if (error) return <Text>Error: {error.message}</Text>;

  return (
    <FlatList
      data={users}
      renderItem={({ item }) => <UserItem user={item} />}
    />
  );
}

// Mutations with automatic cache updates
function CreateUserScreen() {
  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: async (newUser) => {
      const response = await fetch('https://api.example.com/users', {
        method: 'POST',
        body: JSON.stringify(newUser),
      });
      return response.json();
    },
    onSuccess: () => {
      // Invalidate and refetch users list
      queryClient.invalidateQueries({ queryKey: ['users'] });
    },
  });

  const handleSubmit = () => {
    mutation.mutate({ name: 'John', email: 'john@example.com' });
  };

  return (
    <Button
      title="Create User"
      onPress={handleSubmit}
      disabled={mutation.isLoading}
    />
  );
}

Image Caching

// React Native Fast Image
// npm install react-native-fast-image
import FastImage from 'react-native-fast-image';

// Automatically caches images
<FastImage
  style={{ width: 200, height: 200 }}
  source={{
    uri: 'https://example.com/image.jpg',
    priority: FastImage.priority.high,
  }}
  resizeMode={FastImage.resizeMode.cover}
/>

// Preload images
FastImage.preload([
  { uri: 'https://example.com/image1.jpg' },
  { uri: 'https://example.com/image2.jpg' },
]);

// Clear cache when needed
FastImage.clearMemoryCache();
FastImage.clearDiskCache();

Mobile API Best Practices

1. Handle Network Conditions

import NetInfo from '@react-native-community/netinfo';

// Check network status
const checkConnection = async () => {
  const state = await NetInfo.fetch();
  console.log('Connection type:', state.type);
  console.log('Is connected?', state.isConnected);
};

// Listen to network changes
const unsubscribe = NetInfo.addEventListener(state => {
  if (!state.isConnected) {
    showOfflineMessage();
  } else {
    hideOfflineMessage();
    syncPendingChanges();
  }
});

// Show network indicator
function NetworkIndicator() {
  const [isConnected, setIsConnected] = useState(true);

  useEffect(() => {
    const unsubscribe = NetInfo.addEventListener(state => {
      setIsConnected(state.isConnected);
    });
    return () => unsubscribe();
  }, []);

  if (isConnected) return null;

  return (
    <View style={{ backgroundColor: 'red', padding: 10 }}>
      <Text style={{ color: 'white', textAlign: 'center' }}>
        No Internet Connection
      </Text>
    </View>
  );
}

2. Implement Retry Logic

// Retry failed requests
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      const response = await fetch(url, options);
      if (response.ok) return response;

      // Don't retry client errors (400-499)
      if (response.status >= 400 && response.status < 500) {
        throw new Error(`Client error: ${response.status}`);
      }

      // Retry server errors (500+)
      if (i < maxRetries - 1) {
        await delay(1000 * Math.pow(2, i)); // Exponential backoff
        continue;
      }
    } catch (error) {
      if (i === maxRetries - 1) throw error;
      await delay(1000 * Math.pow(2, i));
    }
  }
}

function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

3. Use Request Timeouts

// Set reasonable timeouts
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10s timeout

try {
  const response = await fetch('https://api.example.com/data', {
    signal: controller.signal
  });
  clearTimeout(timeoutId);
  return await response.json();
} catch (error) {
  if (error.name === 'AbortError') {
    console.log('Request timed out');
  }
  throw error;
}

4. Optimize Payload Size

  • Pagination: Load data in chunks, not all at once
  • Compression: Use gzip compression (usually automatic)
  • Selective Fields: Request only needed fields
  • Image Sizes: Request appropriate image sizes for device
  • Lazy Loading: Load data as user scrolls
// Pagination example
const fetchUsers = async (page = 1, limit = 20) => {
  const response = await fetch(
    `https://api.example.com/users?page=${page}&limit=${limit}`
  );
  return response.json();
};

// Infinite scroll
function UserListScreen() {
  const [users, setUsers] = useState([]);
  const [page, setPage] = useState(1);
  const [loading, setLoading] = useState(false);

  const loadMore = async () => {
    if (loading) return;
    setLoading(true);

    const newUsers = await fetchUsers(page);
    setUsers([...users, ...newUsers]);
    setPage(page + 1);
    setLoading(false);
  };

  return (
    <FlatList
      data={users}
      renderItem={({ item }) => <UserItem user={item} />}
      onEndReached={loadMore}
      onEndReachedThreshold={0.5}
      ListFooterComponent={loading && <ActivityIndicator />}
    />
  );
}

5. Secure API Calls

  • Use HTTPS: Always use secure connections
  • Store Tokens Securely: Use SecureStore for auth tokens
  • Token Refresh: Automatically refresh expired tokens
  • Don't Store Secrets: Never hardcode API keys in app
  • Validate Responses: Check data before using it

Common Mistakes to Avoid

  • Not Handling Errors: Always wrap in try-catch
  • Ignoring Loading States: Show spinners during requests
  • No Offline Support: App should work without internet when possible
  • Fetching Too Much Data: Use pagination, not loading thousands of items
  • No Retry Logic: Network requests fail, plan for retries
  • Blocking UI: Use async/await, don't freeze the app
  • Poor Cache Management: Stale data confuses users
  • Not Canceling Requests: Cancel requests when user navigates away

Ready to Master Mobile APIs?

Build robust, offline-first mobile applications that work anywhere

Explore Hybrid Mobile App Course

Related Topics