Package Exports
- rest-api-kit
- rest-api-kit/package.json
Readme
๐ Rest API Kit
The ultimate TypeScript-first REST API management library for React and React Native applications
Take complete control of your API calls, caching, and state management with enterprise-grade features and developer-friendly APIs.
โจ Why Rest API Kit?
- ๐ฏ Zero Configuration - Works out of the box with sensible defaults
- ๐ฅ Type-Safe - Full TypeScript support with intelligent type inference
- โก Performance First - Advanced caching, memoization, and selective updates
- ๐๏ธ Enterprise Ready - Middleware, interceptors, retry logic, and error handling
- ๐ฑ Universal - Works seamlessly in React web apps and React Native mobile apps
- ๐งฉ Modular - Use only what you need, tree-shakeable
- ๐ ๏ธ Developer Experience - Redux DevTools, debugging, and comprehensive error messages
๐ฆ Installation
# npm
npm install rest-api-kit
# yarn
yarn add rest-api-kit
# pnpm
pnpm add rest-api-kitPeer Dependencies
npm install react@^17.0.0 || ^18.0.0๐ Quick Start
1. Create Your API Base
// api/base.ts
import { createRestBase } from 'rest-api-kit';
export const api = createRestBase({
baseUrl: 'https://jsonplaceholder.typicode.com',
prepareHeaders: (headers) => {
// Add authentication, content-type, etc.
const token = localStorage.getItem('authToken'); // or from your auth system
if (token) {
headers.set('Authorization', `Bearer ${token}`);
}
headers.set('Content-Type', 'application/json');
return headers;
},
});2. Define Your Endpoints
// api/endpoints.ts
import { api } from './base';
// Define your API endpoints with full type safety
export const {
useGetTodos,
useCreateTodo,
useUpdateTodo,
useDeleteTodo,
useGetUser,
} = api.createEndpoints((builder) => ({
// GET endpoint with response typing
getTodos: builder<
Todo[], // Response type
void // Request body type (void for GET)
>({
url: '/todos',
params: {
method: 'GET',
preferCacheValue: true, // Use cache if available
saveToCache: true, // Save response to cache
},
}),
// POST endpoint with request/response typing
createTodo: builder<
Todo, // Response type
CreateTodoRequest // Request body type
>({
url: '/todos',
params: {
method: 'POST',
preferCacheValue: false,
saveToCache: false,
updates: ['getTodos'], // Clear getTodos cache after creation
transformResponse: (data, requestBody) => {
// Transform response data
return { ...data, locallyCreated: true };
},
},
}),
// PUT endpoint
updateTodo: builder<
Todo,
UpdateTodoRequest & { id: string }
>({
url: '/todos',
params: {
method: 'PUT',
updates: ['getTodos'],
buildUrl: (baseUrl, body) => `${baseUrl}/${body.id}`,
},
}),
// DELETE endpoint
deleteTodo: builder<
{ success: boolean },
{ id: string }
>({
url: '/todos',
params: {
method: 'DELETE',
updates: ['getTodos'],
buildUrl: (baseUrl, body) => `${baseUrl}/${body.id}`,
},
}),
// GET with parameters
getUser: builder<
User,
{ userId: string }
>({
url: '/users',
params: {
method: 'GET',
preferCacheValue: true,
saveToCache: true,
buildUrl: (baseUrl, body) => `${baseUrl}/${body.userId}`,
},
}),
}));
// Type definitions
interface Todo {
id: number;
title: string;
completed: boolean;
userId: number;
}
interface CreateTodoRequest {
title: string;
completed?: boolean;
userId: number;
}
interface UpdateTodoRequest {
title?: string;
completed?: boolean;
}
interface User {
id: number;
name: string;
email: string;
username: string;
}3. Use in Your Components
// components/TodoList.tsx
import React, { useEffect } from 'react';
import { useGetTodos, useCreateTodo, useDeleteTodo } from '../api/endpoints';
const TodoList: React.FC = () => {
// Destructure trigger function and state
const [getTodos, { data: todos, loading, error }] = useGetTodos();
const [createTodo, createState] = useCreateTodo();
const [deleteTodo, deleteState] = useDeleteTodo();
// Load todos on component mount
useEffect(() => {
getTodos();
}, []);
const handleCreateTodo = async () => {
const result = await createTodo({
title: 'New Todo',
completed: false,
userId: 1,
});
if (result.type === 'success') {
console.log('Todo created:', result.data);
}
};
const handleDeleteTodo = async (id: string) => {
const result = await deleteTodo({ id });
if (result.type === 'success') {
console.log('Todo deleted successfully');
}
};
if (loading) return <div>Loading todos...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
<h1>Todos</h1>
<button onClick={handleCreateTodo} disabled={createState.loading}>
{createState.loading ? 'Creating...' : 'Add Todo'}
</button>
<ul>
{todos?.map((todo) => (
<li key={todo.id}>
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.title}
</span>
<button
onClick={() => handleDeleteTodo(todo.id.toString())}
disabled={deleteState.loading}
>
Delete
</button>
</li>
))}
</ul>
</div>
);
};
export default TodoList;๐ฑ React Native Integration
Rest API Kit works seamlessly with React Native:
// api/base.ts (React Native)
import { createRestBase } from 'rest-api-kit';
import AsyncStorage from '@react-native-async-storage/async-storage';
export const api = createRestBase({
baseUrl: 'https://your-api.com/v1',
prepareHeaders: async (headers) => {
// Get token from AsyncStorage
const token = await AsyncStorage.getItem('authToken');
if (token) {
headers.set('Authorization', `Bearer ${token}`);
}
headers.set('Content-Type', 'application/json');
return headers;
},
});
// components/UserProfile.tsx (React Native)
import React, { useEffect } from 'react';
import { View, Text, ActivityIndicator, TouchableOpacity } from 'react-native';
import { useGetUser } from '../api/endpoints';
const UserProfile: React.FC<{ userId: string }> = ({ userId }) => {
const [getUser, { data: user, loading, error }] = useGetUser();
useEffect(() => {
getUser({ userId });
}, [userId]);
if (loading) {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<ActivityIndicator size="large" />
<Text>Loading user...</Text>
</View>
);
}
if (error) {
return (
<View style={{ padding: 20 }}>
<Text style={{ color: 'red' }}>Error: {error}</Text>
</View>
);
}
return (
<View style={{ padding: 20 }}>
<Text style={{ fontSize: 24, fontWeight: 'bold' }}>{user?.name}</Text>
<Text style={{ fontSize: 16, color: 'gray' }}>{user?.email}</Text>
</View>
);
};
export default UserProfile;๐๏ธ Advanced Features
Middleware System
import { storeMethods } from 'rest-api-kit';
// Logging middleware
const loggingMiddleware = (action, state, next) => {
console.log(`[${new Date().toISOString()}] Action:`, action.type);
const start = Date.now();
next(action);
console.log(`[${Date.now() - start}ms] Completed:`, action.type);
};
// Authentication middleware
const authMiddleware = (action, state, next) => {
if (action.type === 'store/save' && action.payload.id.startsWith('user-')) {
// Encrypt user data before storing
const encryptedData = encrypt(action.payload.data);
next({ ...action, payload: { ...action.payload, data: encryptedData } });
} else {
next(action);
}
};
// Add middleware
storeMethods.addMiddleware(loggingMiddleware);
storeMethods.addMiddleware(authMiddleware);Request Interceptors
import { createRequest, makeRequest } from 'rest-api-kit';
// Custom request with interceptors
const apiClient = new ApiClient({
baseURL: 'https://api.example.com',
timeout: 10000,
retries: 3,
retryDelay: 1000,
});
// Request interceptor
apiClient.addRequestInterceptor({
onRequest: async (config) => {
// Add timestamp to all requests
config.headers = config.headers || {};
config.headers['X-Request-Time'] = Date.now().toString();
return config;
},
onRequestError: async (error) => {
console.error('Request failed:', error);
throw error;
},
});
// Response interceptor
apiClient.addResponseInterceptor({
onResponse: async (response) => {
// Log response time
const requestTime = response.headers.get('X-Request-Time');
if (requestTime) {
console.log(`Request took ${Date.now() - parseInt(requestTime)}ms`);
}
return response;
},
onResponseError: async (error) => {
if (error.status === 401) {
// Handle unauthorized
await refreshToken();
throw error;
}
return error;
},
});Optimistic Updates
const [updateTodo, { loading, error }] = useUpdateTodo();
const handleToggleTodo = async (todo: Todo) => {
// Optimistic update
const optimisticData = { ...todo, completed: !todo.completed };
// Update UI immediately
updateTodoInCache(todo.id, optimisticData);
try {
const result = await updateTodo({
id: todo.id.toString(),
completed: optimisticData.completed,
});
if (result.type === 'error') {
// Revert on error
updateTodoInCache(todo.id, todo);
}
} catch (error) {
// Revert on error
updateTodoInCache(todo.id, todo);
}
};State Management Integration
// Store management
import { useStore, useStoreSelector } from 'rest-api-kit';
const Dashboard: React.FC = () => {
const store = useStore();
// Selective subscriptions for performance
const userCount = useStoreSelector(
state => Object.keys(state).filter(key => key.startsWith('user-')).length
);
const todoCount = useStoreSelector(
state => Object.keys(state).filter(key => key.startsWith('todo-')).length
);
// Batch operations for efficiency
const clearAllData = () => {
store.batch([
{ type: 'store/clear', payload: { id: 'todos' } },
{ type: 'store/clear', payload: { id: 'users' } },
{ type: 'store/clear', payload: { id: 'profile' } },
]);
};
return (
<div>
<h1>Dashboard</h1>
<p>Users: {userCount}</p>
<p>Todos: {todoCount}</p>
<button onClick={clearAllData}>Clear All Data</button>
</div>
);
};๐๏ธ Configuration Options
Base Configuration
const api = createRestBase({
baseUrl: 'https://api.example.com/v1',
prepareHeaders: (headers) => {
// Global headers for all requests
headers.set('Content-Type', 'application/json');
headers.set('Accept', 'application/json');
// Conditional headers
const token = getAuthToken();
if (token) {
headers.set('Authorization', `Bearer ${token}`);
}
// API versioning
headers.set('API-Version', '2023-08-01');
return headers;
},
});Endpoint Parameters
builder<ResponseType, RequestType>({
url: '/endpoint',
params: {
// HTTP method
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH',
// Caching behavior
preferCacheValue: true, // Use cached data if available
saveToCache: true, // Save response to cache
// Cache invalidation
updates: ['endpoint1', 'endpoint2'], // Clear these caches after success
// URL building
buildUrl: (baseUrl, requestBody) => `${baseUrl}/custom/${requestBody.id}`,
// Data transformation
transformResponse: (data, requestBody) => {
// Transform response data
return { ...data, transformed: true };
},
// Success condition
successCondition: (data) => {
// Custom success validation
return data.status === 'ok';
},
// Custom headers for this endpoint
headers: {
'X-Custom-Header': 'value',
},
// Retry configuration
retries: 3,
retryDelay: 1000,
retryCondition: (error) => error.status >= 500,
// Timeout
timeout: 30000,
// Request/Response validation
validateStatus: (status) => status >= 200 && status < 300,
},
})๐ง Advanced Use Cases
File Upload
const useUploadFile = builder<
{ url: string; id: string },
{ file: File; metadata?: object }
>({
url: '/upload',
params: {
method: 'POST',
transformRequest: ({ file, metadata }) => {
const formData = new FormData();
formData.append('file', file);
if (metadata) {
formData.append('metadata', JSON.stringify(metadata));
}
return formData;
},
},
});
// Usage
const [uploadFile, { loading, progress }] = useUploadFile();
const handleFileUpload = async (file: File) => {
const result = await uploadFile({
file,
metadata: { userId: 123, category: 'profile' }
});
};Pagination
const useGetPaginatedTodos = builder<
{ todos: Todo[]; totalCount: number; hasMore: boolean },
{ page: number; limit: number }
>({
url: '/todos',
params: {
method: 'GET',
buildUrl: (baseUrl, { page, limit }) =>
`${baseUrl}?page=${page}&limit=${limit}`,
transformResponse: (data) => ({
todos: data.items,
totalCount: data.total,
hasMore: data.page * data.limit < data.total,
}),
},
});
// Infinite scrolling component
const InfiniteTodoList: React.FC = () => {
const [page, setPage] = useState(1);
const [allTodos, setAllTodos] = useState<Todo[]>([]);
const [getTodos, { data, loading }] = useGetPaginatedTodos();
const loadMoreTodos = async () => {
const result = await getTodos({ page, limit: 20 });
if (result.type === 'success') {
setAllTodos(prev => [...prev, ...result.data.todos]);
setPage(prev => prev + 1);
}
};
useEffect(() => {
loadMoreTodos();
}, []);
return (
<div>
{allTodos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
{data?.hasMore && (
<button onClick={loadMoreTodos} disabled={loading}>
{loading ? 'Loading...' : 'Load More'}
</button>
)}
</div>
);
};Real-time Updates
import { useStoreEvents } from 'rest-api-kit';
const useRealTimeUpdates = () => {
const [getTodos] = useGetTodos();
useStoreEvents((event) => {
// Listen for specific store changes
if (event.type === 'save' && event.payload.id === 'todos') {
// Todos updated, you might want to notify user
showNotification('Todos updated!');
}
});
useEffect(() => {
// WebSocket connection for real-time updates
const ws = new WebSocket('wss://api.example.com/updates');
ws.onmessage = (event) => {
const update = JSON.parse(event.data);
if (update.type === 'todo_updated') {
// Refresh todos when server sends update
getTodos();
}
};
return () => ws.close();
}, []);
};๐งช Testing
Testing Components
// __tests__/TodoList.test.tsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { TodoList } from '../components/TodoList';
// Mock the API hooks
jest.mock('../api/endpoints', () => ({
useGetTodos: () => [
jest.fn(),
{
data: [
{ id: 1, title: 'Test Todo', completed: false, userId: 1 }
],
loading: false,
error: null,
}
],
useCreateTodo: () => [
jest.fn().mockResolvedValue({ type: 'success', data: {} }),
{ loading: false, error: null }
],
}));
test('renders todos and handles creation', async () => {
render(<TodoList />);
expect(screen.getByText('Test Todo')).toBeInTheDocument();
fireEvent.click(screen.getByText('Add Todo'));
await waitFor(() => {
expect(screen.getByText('Creating...')).toBeInTheDocument();
});
});Testing API Integration
// __tests__/api.test.ts
import { createRestBase } from 'rest-api-kit';
import fetchMock from 'jest-fetch-mock';
beforeEach(() => {
fetchMock.enableMocks();
});
afterEach(() => {
fetchMock.resetMocks();
});
test('API integration', async () => {
const api = createRestBase({
baseUrl: 'https://test-api.com',
prepareHeaders: (headers) => headers,
});
const { useGetTodos } = api.createEndpoints((builder) => ({
getTodos: builder<Todo[], void>({
url: '/todos',
params: { method: 'GET' },
}),
}));
fetchMock.mockResponseOnce(
JSON.stringify([{ id: 1, title: 'Test', completed: false }]),
{ headers: { 'content-type': 'application/json' } }
);
// Test the API call
const [getTodos] = useGetTodos();
const result = await getTodos();
expect(result.type).toBe('success');
expect(result.data).toHaveLength(1);
});๐ Performance Optimization
Bundle Size Optimization
// Import only what you need for smaller bundles
import { createRestBase } from 'rest-api-kit';
import { useStore } from 'rest-api-kit/store';
import { createRequest } from 'rest-api-kit/core';Memory Management
// Automatic cleanup
const MyComponent: React.FC = () => {
const [getTodos, state] = useGetTodos();
// Component automatically cleans up subscriptions on unmount
// No manual cleanup required!
return <div>{/* component content */}</div>;
};
// Manual cache management for large apps
const clearUnusedCache = () => {
const store = useStore();
// Clear old data
const oneHourAgo = Date.now() - 60 * 60 * 1000;
const entries = store.entries();
entries.forEach(([key, value]) => {
if (value.timestamp && value.timestamp < oneHourAgo) {
store.clear(key);
}
});
};๐ ๏ธ Migration Guide
From Fetch/Axios
// โ Before (fetch)
const [todos, setTodos] = useState([]);
const [loading, setLoading] = useState(false);
const fetchTodos = async () => {
setLoading(true);
try {
const response = await fetch('/api/todos');
const data = await response.json();
setTodos(data);
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
};
// โ
After (Rest API Kit)
const [getTodos, { data: todos, loading, error }] = useGetTodos();
useEffect(() => {
getTodos();
}, []);From Redux Toolkit Query
// โ Before (RTK Query)
const todosApi = createApi({
reducerPath: 'todosApi',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
endpoints: (builder) => ({
getTodos: builder.query<Todo[], void>({
query: () => 'todos',
}),
}),
});
// โ
After (Rest API Kit)
const { useGetTodos } = api.createEndpoints((builder) => ({
getTodos: builder<Todo[], void>({
url: '/todos',
params: { method: 'GET' },
}),
}));๐ API Reference
Core Functions
createRestBase(options)
Creates the base API instance.
Parameters:
baseUrl: string - Base URL for all requestsprepareHeaders: (headers: Headers) => Headers - Global header preparation
builder<ResponseType, RequestType>(config)
Creates a typed endpoint configuration.
Type Parameters:
ResponseType: Type of the API responseRequestType: Type of the request body/parameters
Hook Returns
const [triggerFunction, state] = useEndpoint();Trigger Function:
- Returns:
Promise<ApiResponse<ResponseType>> - Parameters: RequestType (if not void)
State Object:
data: ResponseType | undefinedloading: booleanerror: string | nulllastFetched: number | null
Store Functions
useStore()
save(id, data, options?): Save data to storeget(id): Retrieve data from storeupdate(id, partialData): Update existing dataclear(id): Remove specific dataclearAll(): Remove all databatch(operations): Perform multiple operations
useStoreSelector(selector, equalityFn?)
Subscribe to specific store data with performance optimization.
useStoreEvents(listener, options?)
Listen to store events for debugging and logging.
๐ค Contributing
We welcome contributions! Please see our Contributing Guide for details.
Development Setup
git clone https://github.com/naries/rest-api-kit.git
cd rest-api-kit
npm install
npm run test
npm run build๐ License
MIT ยฉ naries
๐ Support
- ๐ Documentation
- ๐ Issue Tracker
- ๐ฌ Discussions
- ๐ง Email: support@rest-api-kit.dev
Made with โค๏ธ by developers, for developers