Learn how to use the API client and storage utilities for backend integration.
This project includes minimal, unopinionated utilities for:
- API calls - Axios-based client with interceptors
- Local storage - AsyncStorage wrapper with JSON serialization
These are simple foundations you can build upon, not a complete API layer.
The API client is configured in services/api.ts with:
- Base URL from environment variable (
EXPO_PUBLIC_API_URL) - Request interceptor for authentication tokens
- Response interceptor for error handling
- Default JSON headers
Set your API URL in .env:
EXPO_PUBLIC_API_URL=https://api.example.comThe client defaults to JSONPlaceholder (https://jsonplaceholder.typicode.com) if no URL is set.
import { todosApi, userApi } from '@/services/api';
// Fetch all todos
const todos = await todosApi.getAll();
// Fetch todos by user ID
const userTodos = await todosApi.getByUserId(1);
// Fetch all users
const users = await userApi.getAll();Use the default export for custom API calls:
import api from '@/services/api';
// GET request
const response = await api.get('/custom-endpoint');
const data = response.data;
// POST request
const response = await api.post('/custom-endpoint', {
name: 'John',
email: 'john@example.com',
});The API client automatically adds authentication tokens from storage:
- Store token using storage service:
import { setItem, STORAGE_KEYS } from '@/services/storage';
await setItem(STORAGE_KEYS.AUTH_TOKEN, 'your-token-here');-
Token is automatically added to
Authorization: Bearer <token>header on all requests. -
Remove token on logout:
import { removeItem, STORAGE_KEYS } from '@/services/storage';
await removeItem(STORAGE_KEYS.AUTH_TOKEN);Errors are automatically formatted with consistent structure:
import { todosApi } from '@/services/api';
import type { ApiError } from '@/types/api';
try {
const todos = await todosApi.getAll();
} catch (error) {
const apiError = error as ApiError;
console.error(apiError.message); // Error message
console.error(apiError.status); // HTTP status code
console.error(apiError.data); // Response data
}Create new API modules following the pattern:
// services/api.ts
export const postsApi = {
getAll: async () => {
const response = await api.get('/posts');
return response.data;
},
getById: async (id: number) => {
const response = await api.get(`/posts/${id}`);
return response.data;
},
create: async (post: { title: string; body: string }) => {
const response = await api.post('/posts', post);
return response.data;
},
};The storage service (services/storage.ts) provides a simple wrapper around AsyncStorage with:
- JSON serialization/deserialization
- TypeScript generic support
- Common storage keys constant
import { setItem, STORAGE_KEYS } from '@/services/storage';
// Store simple value
await setItem(STORAGE_KEYS.AUTH_TOKEN, 'token-123');
// Store object
await setItem(STORAGE_KEYS.USER_DATA, {
id: 1,
name: 'John',
email: 'john@example.com',
});import { getItem, STORAGE_KEYS } from '@/services/storage';
// Get with type safety
const token = await getItem<string>(STORAGE_KEYS.AUTH_TOKEN);
const user = await getItem<User>(STORAGE_KEYS.USER_DATA);
// Check if value exists
if (token) {
console.log('Token:', token);
}import { removeItem, STORAGE_KEYS } from '@/services/storage';
// Remove specific item
await removeItem(STORAGE_KEYS.AUTH_TOKEN);
// Clear all storage
import { clear } from '@/services/storage';
await clear();Use predefined keys from STORAGE_KEYS constant:
import { STORAGE_KEYS } from '@/services/storage';
STORAGE_KEYS.AUTH_TOKEN; // 'auth_token'
STORAGE_KEYS.USER_DATA; // 'user_data'
STORAGE_KEYS.SETTINGS; // 'settings'Add custom keys as needed:
const CUSTOM_KEY = 'my_custom_key';
await setItem(CUSTOM_KEY, { data: 'value' });Here's a complete example combining API and storage:
import { useState, useEffect } from 'react';
import { todosApi } from '@/services/api';
import { getItem, setItem, STORAGE_KEYS } from '@/services/storage';
import type { Todo } from '@/types/api';
function TodosScreen() {
const [todos, setTodos] = useState<Todo[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
loadTodos();
}, []);
const loadTodos = async () => {
setLoading(true);
try {
// Check cache first
const cached = await getItem<Todo[]>(STORAGE_KEYS.TODOS);
if (cached) {
setTodos(cached);
}
// Fetch from API
const data = await todosApi.getAll();
setTodos(data);
// Cache results
await setItem(STORAGE_KEYS.TODOS, data);
} catch (error) {
console.error('Failed to load todos:', error);
} finally {
setLoading(false);
}
};
return (
// Your UI here
);
}To switch from JSONPlaceholder to your real backend:
- Update
.envfile:
EXPO_PUBLIC_API_URL=https://api.yourbackend.com-
Update API types in
types/api.tsto match your backend responses -
Update endpoint functions in
services/api.tsto match your API structure -
Test authentication flow - ensure tokens are stored and sent correctly
The interceptors and error handling will work the same way with your real backend.
- Always handle errors - Use try/catch blocks around API calls
- Show loading states - Provide user feedback during API calls
- Type your responses - Use TypeScript interfaces for API responses
- Cache when appropriate - Use storage for offline support or faster loads
- Keep it simple - These utilities are minimal by design; add complexity only when needed
- Getting Started - Initial setup and project overview
- How-To Guides - Common tasks including adding API endpoints
- Code Conventions - Project standards and best practices
- Environment Variables Guide - How to configure environment variables
- Store Data Guide - More information on data persistence