React Native REST API Integration with Axios and Context API

by Didin J. on Dec 13, 2025 React Native REST API Integration with Axios and Context API

Learn how to integrate REST APIs in React Native using Axios and Context API with TypeScript. Build a scalable CRUD app with clean architecture.

Modern mobile applications rarely work in isolation. Most React Native apps need to communicate with REST APIs to fetch data, submit forms, authenticate users, and synchronize state with a backend server.

While React Native provides basic networking capabilities via fetch, real-world apps often require:

  • Request/response interceptors

  • Centralized API configuration

  • Global state for fetched data

  • Clean separation between UI and data logic

In this tutorial, you’ll learn how to build a scalable REST API integration in React Native using:

  • Axios for HTTP requests

  • React Context API for global state management

  • A clean, reusable project structure suitable for production apps

By the end of this guide, you will be able to:

  • Configure Axios with a base URL and interceptors

  • Create a global API context using Context API

  • Fetch, create, update, and delete data from a REST API

  • Handle loading, errors, and API responses cleanly

  • Build a reusable pattern you can apply to any React Native app

What We’ll Build

We’ll build a simple Posts App that interacts with a REST API:

Features:

  • Fetch a list of posts from an API

  • Display posts in a FlatList

  • Handle loading and error states

  • Share API data globally using Context API

Tech Stack:

  • React Native (latest, Community CLI)

  • Axios

  • React Context API

  • REST API (JSONPlaceholder for demo)

Prerequisites

Before starting, make sure you have:

  • Node.js 18+

  • React Native CLI environment set up

  • Basic knowledge of:

    • React Hooks (useState, useEffect, useContext)

    • JavaScript ES6+

    • REST APIs (GET, POST, PUT, DELETE)


Project Setup

In this section, we’ll create a new React Native project, install Axios, and prepare a clean folder structure that works well with the Context API.

1. Create a New React Native App

We’ll use the React Native Community CLI (recommended for production apps).

npx @react-native-community/cli init RnAxiosContextApp
cd RnAxiosContextApp

Start the app to ensure everything works:

npx react-native run-android
# or
npx react-native run-ios

You should see the default React Native welcome screen.

2. Install Axios

Axios will handle all HTTP requests to our REST API.

npm install axios react-native-safe-area-context

No additional native linking is required.

3. TypeScript-Friendly Project Structure

We’ll use .ts and .tsx files:

src/
 ├── api/
 │   └── axios.ts
 ├── context/
 │   └── PostContext.tsx
 ├── screens/
 │   └── PostListScreen.tsx
 ├── components/
 │   └── PostItem.tsx
 ├── types/
 │   └── post.ts
 └── App.tsx

Why add types/?

  • Keeps API models clean and reusable

  • Makes Context and Axios responses strongly typed

4. Update App Entry (TypeScript)

src/App.tsx

import React, { ReactNode } from 'react';
import { StyleSheet } from 'react-native';
import { PostProvider } from './context/PostContext';
import PostListScreen from './screens/PostListScreen';
import { SafeAreaView } from 'react-native-safe-area-context';

const App = (): ReactNode => {
  return (
    <PostProvider>
      <SafeAreaView style={styles.container}>
        <PostListScreen />
      </SafeAreaView>
    </PostProvider>
  );
};

export default App;

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
});

This ensures:

  • All screens can access API data via Context

  • Global state is initialized at the app root

5. Add Path Alias for TypeScript (Recommended)

Update tsconfig.json:

{
  "extends": "@react-native/typescript-config",
  "compilerOptions": {
    "types": ["jest"],
    "baseUrl": "./src",
    "paths": {
      "@api/*": ["api/*"],
      "@context/*": ["context/*"],
      "@screens/*": ["screens/*"],
      "@components/*": ["components/*"],
      "@types/*": ["types/*"]
    }
  },
  "include": ["**/*.ts", "**/*.tsx"],
  "exclude": ["**/node_modules", "**/Pods"]
}

Now imports are clean and readable:

import api from '@api/axios';
import { Post } from '@types/post';

💡 Restart Metro after changing tsconfig.json:

npx react-native start --reset-cache

6. Define the Post Type

src/types/post.ts

export interface Post {
  id: number;
  title: string;
  body: string;
}

This will be reused across:

  • Axios responses

  • Context state

  • UI components

7. Verify TypeScript Setup

At this stage:

  • Project runs with TypeScript

  • Axios is installed and typed

  • App is wrapped with a Context Provider

  • Types are centralized and reusable


Configuring Axios (TypeScript)

In this section, we’ll create a centralized, typed Axios instance that will be used across the entire React Native app. This approach keeps networking logic clean, reusable, and easy to maintain.

1. Why Use a Custom Axios Instance?

Instead of calling axios.get() everywhere, we create a single Axios instance to:

  • Define a base URL once

  • Set default headers

  • Add request and response interceptors

  • Handle errors consistently

  • Keep everything strongly typed with TypeScript

This pattern scales very well for real-world apps.

2. Choose a REST API

For this tutorial, we’ll use JSONPlaceholder, a free fake REST API:

https://jsonplaceholder.typicode.com

We’ll fetch posts from:

GET /posts

3. Create the Axios Instance

Create the Axios configuration file.

src/api/axios.ts

import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';

const api: AxiosInstance = axios.create({
  baseURL: 'https://jsonplaceholder.typicode.com',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json',
  },
});

export default api;

What this does:

  • baseURL → Avoid repeating API URLs

  • timeout → Prevents hanging requests

  • Content-Type → Default JSON handling

  • AxiosInstance → Ensures strong typing

4. Add a Request Interceptor

Request interceptors run before every request. They’re commonly used for:

  • Adding auth tokens

  • Logging requests

  • Modifying headers dynamically

Update src/api/axios.ts:

import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios';

const api: AxiosInstance = axios.create({
    baseURL: 'https://jsonplaceholder.typicode.com',
    timeout: 10000,
    headers: {
        'Content-Type': 'application/json',
    },
});

api.interceptors.request.use(
    (config: InternalAxiosRequestConfig) => {
        // Example: attach auth token
        // const token = 'your-auth-token';
        // if (token) {
        //   config.headers.Authorization = `Bearer ${token}`;
        // }

        console.log(
            `[API REQUEST] ${config.method?.toUpperCase()} ${config.url}`
        );

        return config;
    },
    error => {
        return Promise.reject(error);
    }
);

export default api;

5. Add a Response Interceptor

Response interceptors handle:

  • API responses

  • Global error handling

  • Logging responses

Extend src/api/axios.ts:

api.interceptors.response.use(
  (response: AxiosResponse) => {
    console.log(
      `[API RESPONSE] ${response.status} ${response.config.url}`
    );
    return response;
  },
  error => {
    if (error.response) {
      console.error(
        `[API ERROR] ${error.response.status}`,
        error.response.data
      );
    } else if (error.request) {
      console.error('[API ERROR] No response received');
    } else {
      console.error('[API ERROR]', error.message);
    }

    return Promise.reject(error);
  }
);

This gives us centralized error logging for every API call.

6. Create a Typed API Function

Now let’s create a typed helper function to fetch posts.

Update src/api/axios.ts:

import { Post } from '@types/post';

export const fetchPosts = async (): Promise<Post[]> => {
  const response = await api.get<Post[]>('/posts');
  return response.data;
};

Why this matters:

  • Post[] ensures correct response shape

  • TypeScript will warn you if the API data changes

  • IDE auto-completion improves productivity

7. Axios Configuration Summary

At this point, we have:

  • A reusable Axios instance

  • Base URL and default headers

  • Request & response interceptors

  • Typed API helper functions

This is the foundation for connecting Axios with the Context API.


Creating the Context API (TypeScript)

In this section, we’ll build a typed Context API to manage:

  • Global post data

  • Loading state

  • Error handling

  • API calls via Axios

This keeps networking logic out of UI components and makes the app easier to maintain.

1. Why Use Context API for API Data?

Using Context API allows us to:

  • Share API data across multiple screens

  • Avoid prop drilling

  • Centralize business logic

  • Keep UI components clean and focused

For small-to-medium apps, Context API is often enough without adding Redux or other libraries.

2. Define Context Types

Create the context file.

src/context/PostContext.tsx

import React, {
  createContext,
  useContext,
  useEffect,
  useState,
  ReactNode,
} from 'react';
import { fetchPosts } from '@api/axios';
import { Post } from '@types/post';

interface PostContextState {
  posts: Post[];
  loading: boolean;
  error: string | null;
  loadPosts: () => Promise<void>;
}

const PostContext = createContext<PostContextState | undefined>(undefined);

3. Create the Provider Component

Still in PostContext.tsx, add the provider implementation.

export const PostProvider = ({ children }: { children: ReactNode }) => {
  const [posts, setPosts] = useState<Post[]>([]);
  const [loading, setLoading] = useState<boolean>(false);
  const [error, setError] = useState<string | null>(null);

  const loadPosts = async () => {
    try {
      setLoading(true);
      setError(null);

      const data = await fetchPosts();
      setPosts(data);
    } catch (err: any) {
      setError('Failed to load posts');
    } finally {
      setLoading(false);
    }
  };

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

  return (
    <PostContext.Provider
      value={{
        posts,
        loading,
        error,
        loadPosts,
      }}
    >
      {children}
    </PostContext.Provider>
  );
};

What’s happening here:

  • State is strongly typed (Post[], boolean, string | null)

  • loadPosts() calls Axios through the API layer

  • Data is fetched automatically on app startup

  • Error handling is centralized

4. Create a Custom Hook for the Context

To avoid repetitive useContext logic, we’ll create a custom hook.

Add this to the bottom of PostContext.tsx:

export const usePosts = (): PostContextState => {
  const context = useContext(PostContext);

  if (!context) {
    throw new Error('usePosts must be used within a PostProvider');
  }

  return context;
};

Now components can simply call usePosts().

5. Context API Architecture Recap

We now have:

  • A typed context state

  • A provider wrapping the app

  • Centralized API calls

  • A reusable custom hook

Data flow:

Axios → API Helper → Context → UI Components


Consuming Context in UI (TypeScript)

In this section, we’ll:

  • Consume the PostContext using the custom hook

  • Display posts in a FlatList

  • Handle loading and error states cleanly

  • Create a reusable post item component

This keeps the UI simple and declarative.

1. Create the Post List Screen

Create the screen component.

src/screens/PostListScreen.tsx

import React from 'react';
import {
  View,
  Text,
  FlatList,
  ActivityIndicator,
  StyleSheet,
  Button,
} from 'react-native';
import { usePosts } from '@context/PostContext';
import PostItem from '@components/PostItem';

const PostListScreen: React.FC = () => {
  const { posts, loading, error, loadPosts } = usePosts();

  if (loading) {
    return (
      <View style={styles.center}>
        <ActivityIndicator size="large" />
      </View>
    );
  }

  if (error) {
    return (
      <View style={styles.center}>
        <Text style={styles.error}>{error}</Text>
        <Button title="Retry" onPress={loadPosts} />
      </View>
    );
  }

  return (
    <FlatList
      data={posts}
      keyExtractor={item => item.id.toString()}
      renderItem={({ item }) => <PostItem post={item} />}
      contentContainerStyle={styles.list}
    />
  );
};

export default PostListScreen;

const styles = StyleSheet.create({
  center: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  error: {
    color: 'red',
    marginBottom: 12,
  },
  list: {
    padding: 16,
  },
});

Key points:

  • UI reacts to loading and error state

  • No API logic in the screen

  • Data comes directly from Context

2. Create the Post Item Component

Now let’s create a reusable component to display a single post.

src/components/PostItem.tsx

import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { Post } from '@types/post';

interface Props {
  post: Post;
}

const PostItem: React.FC<Props> = ({ post }) => {
  return (
    <View style={styles.card}>
      <Text style={styles.title}>{post.title}</Text>
      <Text style={styles.body}>{post.body}</Text>
    </View>
  );
};

export default PostItem;

const styles = StyleSheet.create({
  card: {
    backgroundColor: '#fff',
    padding: 16,
    borderRadius: 8,
    marginBottom: 12,
    elevation: 2,
  },
  title: {
    fontWeight: 'bold',
    marginBottom: 8,
  },
  body: {
    color: '#555',
  },
});

3. UI + Context Integration Flow

Here’s how everything connects:

PostProvider
   ↓
PostContext
   ↓
usePosts()
   ↓
PostListScreen
   ↓
PostItem

This architecture ensures:

  • Components stay stateless and reusable

  • API logic lives in one place

  • Type safety across the entire app

4. Test the App

Run the app again:

npx react-native run-android
# or
npx react-native run-ios

You should see:

  • A loading spinner

  • A list of posts loaded from the REST API

  • Retry button on network error


Handling Create, Update, and Delete (CRUD)

So far, we’ve only read data from the API. In real applications, you’ll also need to:

  • Create new data (POST)

  • Update existing data (PUT / PATCH)

  • Delete data (DELETE)

In this section, we’ll extend:

  • Axios API helpers

  • Context API state

  • Keep everything fully typed with TypeScript

We’ll still use JSONPlaceholder (note: POST/PUT/DELETE are mocked, but perfect for learning).

1. Add CRUD API Functions (Axios)

First, extend the API layer.

src/api/axios.ts

import api from './axios';
import { Post } from '@types/post';

// CREATE
export const createPost = async (
  post: Omit<Post, 'id'>
): Promise<Post> => {
  const response = await api.post<Post>('/posts', post);
  return response.data;
};

// UPDATE
export const updatePost = async (
  id: number,
  post: Omit<Post, 'id'>
): Promise<Post> => {
  const response = await api.put<Post>(`/posts/${id}`, post);
  return response.data;
};

// DELETE
export const deletePost = async (id: number): Promise<void> => {
  await api.delete(`/posts/${id}`);
};

Why Omit<Post, 'id'>?

  • The backend usually generates the id

  • TypeScript prevents accidental misuse

  • Cleaner API contracts

2. Extend Context State Interface

Now we’ll add CRUD methods to the context.

src/context/PostContext.tsx

Update the interface:

interface PostContextState {
  posts: Post[];
  loading: boolean;
  error: string | null;
  loadPosts: () => Promise<void>;
  addPost: (post: Omit<Post, 'id'>) => Promise<void>;
  editPost: (id: number, post: Omit<Post, 'id'>) => Promise<void>;
  removePost: (id: number) => Promise<void>;
}

3. Import CRUD API Functions

At the top of PostContext.tsx:

import {
  fetchPosts,
  createPost,
  updatePost,
  deletePost,
} from '@api/axios';

4. Implement Create, Update, and Delete in Context

Inside PostProvider:

const addPost = async (post: Omit<Post, 'id'>) => {
  try {
    setLoading(true);
    setError(null);

    const newPost = await createPost(post);
    setPosts(prev => [newPost, ...prev]);
  } catch {
    setError('Failed to create post');
  } finally {
    setLoading(false);
  }
};

const editPost = async (id: number, post: Omit<Post, 'id'>) => {
  try {
    setLoading(true);
    setError(null);

    const updated = await updatePost(id, post);
    setPosts(prev =>
      prev.map(p => (p.id === id ? updated : p))
    );
  } catch {
    setError('Failed to update post');
  } finally {
    setLoading(false);
  }
};

const removePost = async (id: number) => {
  try {
    setLoading(true);
    setError(null);

    await deletePost(id);
    setPosts(prev => prev.filter(p => p.id !== id));
  } catch {
    setError('Failed to delete post');
  } finally {
    setLoading(false);
  }
};

5. Expose CRUD Functions via Provider

Update the provider value:

<PostContext.Provider
  value={{
    posts,
    loading,
    error,
    loadPosts,
    addPost,
    editPost,
    removePost,
  }}
>
  {children}
</PostContext.Provider>

Now every screen can create, update, or delete posts via Context.

6. Example: Adding a Post (UI Usage)

Here’s a simple example of using addPost inside a screen or component:

const { addPost } = usePosts();

const handleAddPost = () => {
  addPost({
    title: 'New Post',
    body: 'This post was created using Context API',
  });
};

No Axios calls.
No API logic in UI.
Everything flows through Context.

7. CRUD Architecture Recap

UI Component
   ↓
Context Method (addPost / editPost / removePost)
   ↓
Axios API Helper
   ↓
REST API

This pattern:

  • Scales well

  • Is easy to test

  • Keeps business logic centralized

  • Remains fully type-safe


Error Handling, Typing, and Best Practices

In this section, we’ll improve our implementation by:

  • Handling Axios errors properly and safely

  • Improving TypeScript typing

  • Applying real-world best practices for React Native apps

These refinements make your app more robust and maintainable.

1. Properly Typing Axios Errors

Axios throws errors with a specific shape. Instead of using any, we should use AxiosError.

Create a Reusable API Error Type

src/types/apiError.ts

export interface ApiError {
  message: string;
  status?: number;
}

Create an Error Parser Utility

src/api/errorHandler.ts

import { AxiosError } from 'axios';
import { ApiError } from '../types/apiError';

export const parseApiError = (error: unknown): ApiError => {
    if (error instanceof AxiosError) {
        return {
            message:
                (error.response?.data as any)?.message ||
                error.message ||
                'Unexpected API error',
            status: error.response?.status,
        };
    }

    return {
        message: 'Something went wrong',
    };
};

This ensures:

  • No unsafe any

  • Consistent error shape across the app

  • Better debugging and UI messaging

2. Use Typed Errors in Context

Update PostContext.tsx.

Import the parser

import { parseApiError } from '@api/errorHandler';
import { ApiError } from '@types/apiError';

Update error state type

const [error, setError] = useState<ApiError | null>(null);

Update error handling (example: loadPosts)

const loadPosts = async () => {
  try {
    setLoading(true);
    setError(null);

    const data = await fetchPosts();
    setPosts(data);
  } catch (err) {
    setError(parseApiError(err));
  } finally {
    setLoading(false);
  }
};

Repeat this pattern for addPost, editPost, and removePost.

3. Update UI to Handle Typed Errors

Update PostListScreen.tsx:

  if (error) {
    return (
      <View style={styles.center}>
        <Text style={styles.error}>{error}</Text>
        <Button title="Retry" onPress={loadPosts} />
      </View>
    );
  }

Now the UI works with a strongly typed error object, not strings.

4. Avoid Unnecessary Re-Renders

Wrap context methods with useCallback:

const loadPosts = useCallback(async () => {
  ...
}, []);

And memoize the context value:

const value = useMemo(
  () => ({
    posts,
    loading,
    error,
    loadPosts,
    addPost,
    editPost,
    removePost,
  }),
  [posts, loading, error]
);

Then pass:

<PostContext.Provider value={value}>

This improves performance in larger apps.

5. Best Practices Summary

✅ API Layer

  • Use a single Axios instance

  • Centralize base URL and interceptors

  • Type every request and response

✅ Context API

  • Keep business logic out of UI

  • Expose only necessary methods

  • Use custom hooks (usePosts)

✅ TypeScript

  • Avoid any

  • Use utility types (Omit, Partial)

  • Create shared domain models

✅ UI Layer

  • Handle loading and error states explicitly

  • Keep components presentational

  • Avoid direct API calls

6. When to Move Beyond Context API?

Context API works great for:

  • Small to medium apps

  • Simple global state

  • Low-frequency updates

Consider alternatives when:

  • State becomes complex

  • Performance issues arise

  • You need caching or background sync

Next steps options:

  • React Query (TanStack Query)

  • Zustand

  • Redux Toolkit


Conclusion

In this tutorial, you built a scalable and type-safe REST API integration in React Native using Axios and the Context API. Instead of scattering API calls throughout your components, you implemented a clean architecture that separates concerns and is ready for real-world production use.

What You’ve Accomplished

By following this guide, you learned how to:

  • Set up a React Native TypeScript project the right way

  • Configure a centralized Axios instance with interceptors

  • Create a typed API layer for REST endpoints

  • Manage global API state using Context API

  • Implement full CRUD operations (Create, Read, Update, Delete)

  • Handle loading, errors, and retries gracefully

  • Improve reliability with proper error typing

  • Apply best practices for performance and maintainability

The final data flow looks like this:

UI Components
   ↓
Custom Hook (usePosts)
   ↓
Context Provider
   ↓
Axios API Layer
   ↓
REST API

This pattern keeps your UI declarative, your logic centralized, and your codebase easy to evolve.

When to Use This Approach

This Axios + Context API pattern is ideal when:

  • You’re building a small to medium React Native app

  • You want a global API state without extra dependencies

  • You prefer explicit, readable architecture

  • You value TypeScript safety and clean code

It’s especially useful for:

  • CRUD-based apps

  • Admin dashboards

  • Internal tools

  • Prototypes that may later scale

When to Consider Other Solutions

As your app grows, you may want to evaluate:

  • TanStack Query (React Query) for caching and background syncing

  • Zustand for lightweight global state

  • Redux Toolkit for very complex state flows

The architecture you learned here will still help you understand and adopt those tools more effectively.

Final Thoughts

Axios combined with the Context API provides a powerful yet simple foundation for REST API integration in React Native. By layering your app correctly and embracing TypeScript, you’ve created a solution that’s:

  • Easy to read

  • Easy to test

  • Easy to scale

You can now confidently apply this pattern to your own projects or extend it with authentication, pagination, or offline support.

You can find the full source code on our GitHub.

That's just the basics. If you need more deep learning about React Native, you can take the following cheap course:

Thanks!