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
loadinganderrorstate -
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:
- React Native - The Practical Guide [2025]
- The Complete React Native + Hooks Course
- The Best React Native Course 2025 (From Beginner To Expert)
- React Native: Mobile App Development (CLI) [2025]
- React - The Complete Guide 2025 (incl. Next.js, Redux)
- React Native Development Simplified [2025]
- React Native by Projects: From Basics to Pro [2025]
- Full Stack Ecommerce Mobile App: React Native & Node.js 2025
- Learning Management System Mobile App using React Native
- React Native: Advanced Concepts
Thanks!
