learningpad-api-client
v1.0.0
Published
A powerful, type-safe API client built on top of React Query and Axios with automatic token management and error handling
Maintainers
Readme
@learningpad/api-client
A powerful, type-safe API client built on top of React Query and Axios with automatic token management, error handling, and seamless integration with React applications.
Features
- 🚀 Built on React Query - Leverages the power of TanStack Query for caching, background updates, and more
- 🔐 Automatic Token Management - Handles access token refresh automatically
- 🛡️ Type-Safe - Full TypeScript support with comprehensive type definitions
- 🎯 Service-Oriented - Organize your APIs by service with easy configuration
- 🔄 Smart Retry Mechanism - Only one token refresh happens even when multiple APIs fail with 401 simultaneously
- 📱 React Hooks - Easy-to-use hooks for queries and mutations
- 🎨 Customizable - Flexible configuration and error handling
- 📦 Zero Dependencies - Only requires React Query and Axios as peer dependencies
- 🌐 Universal - Works in both browser and Node.js environments
Installation
npm install @learningpad/api-client @tanstack/react-query axios
# or
yarn add @learningpad/api-client @tanstack/react-query axios
# or
pnpm add @learningpad/api-client @tanstack/react-query axiosQuick Start
1. Setup React Query Provider
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ApiConfig } from "@learningpad/api-client";
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
{/* Your app components */}
</QueryClientProvider>
);
}2. Initialize API Client
import { ApiConfig } from "@learningpad/api-client";
// Initialize the API client
ApiConfig.initialize({
services: {
auth: {
name: "auth",
baseURL: "https://api.example.com/auth",
},
api: {
name: "api",
baseURL: "https://api.example.com/v1",
},
},
defaultTimeout: 30000,
onUnauthorized: () => {
// Handle unauthorized access
window.location.href = "/login";
},
});3. Use in Components
import { useApiQuery, useApiMutation } from "@learningpad/api-client";
function UserProfile() {
// Query data
const {
data: user,
isLoading,
error,
} = useApiQuery({
serviceName: "api",
key: ["user", "profile"],
url: "/user/profile",
});
// Mutate data
const updateProfile = useApiMutation({
serviceName: "api",
url: "/user/profile",
method: "put",
successMessage: "Profile updated successfully!",
});
const handleUpdate = (data: any) => {
updateProfile.mutate(data);
};
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h1>{user?.name}</h1>
<button onClick={() => handleUpdate({ name: "New Name" })}>
Update Profile
</button>
</div>
);
}Configuration
Basic Configuration
import { ApiConfig } from "@learningpad/api-client";
ApiConfig.initialize({
services: {
// Define your API services
auth: {
name: "auth",
baseURL: "https://auth.example.com",
timeout: 10000,
headers: {
"X-API-Version": "1.0",
},
},
api: {
name: "api",
baseURL: "https://api.example.com/v1",
timeout: 30000,
},
},
defaultTimeout: 30000,
defaultHeaders: {
"Content-Type": "application/json",
},
});Advanced Configuration with Custom Token Manager
import { ApiConfig, TokenManager } from "@learningpad/api-client";
const customTokenManager: TokenManager = {
getAccessToken: () => {
// Your custom token retrieval logic
return localStorage.getItem("access_token");
},
getRefreshToken: () => {
return localStorage.getItem("refresh_token");
},
setAccessToken: (token: string) => {
localStorage.setItem("access_token", token);
},
setRefreshToken: (token: string) => {
localStorage.setItem("refresh_token", token);
},
clearTokens: () => {
localStorage.removeItem("access_token");
localStorage.removeItem("refresh_token");
},
};
const customNotificationManager = {
success: (message: string) => toast.success(message),
error: (message: string) => toast.error(message),
info: (message: string) => toast.info(message),
warning: (message: string) => toast.warning(message),
};
ApiConfig.initialize({
services: {
auth: {
name: "auth",
baseURL: "https://auth.example.com",
},
api: {
name: "api",
baseURL: "https://api.example.com/v1",
},
},
tokenManager: customTokenManager,
notificationManager: customNotificationManager,
onUnauthorized: () => {
// Redirect to login
window.location.href = "/login";
},
onTokenRefresh: (newToken: string) => {
console.log("Token refreshed:", newToken);
},
onTokenRefreshError: (error: Error) => {
console.error("Token refresh failed:", error);
},
});Smart Retry Mechanism
The API client includes an intelligent retry mechanism that prevents multiple token refresh requests when multiple APIs fail with 401 errors simultaneously.
How it works
- Multiple API calls fail with 401 - When 4 API calls fail with 401 at the same time
- Only one token refresh - Only the first call triggers a token refresh
- Other calls wait - The remaining 3 calls wait for the refresh to complete
- All calls retry - Once the new token is obtained, ALL calls retry with the new token
- All calls succeed - All calls succeed (assuming the refresh was successful)
Example
// These 4 calls will all fail with 401 simultaneously
// The retry mechanism ensures only ONE token refresh happens
// All 4 calls will wait for that single refresh and then retry
const promises = [
apiClient.getService("api").get("/users/profile"),
apiClient.getService("api").get("/users/settings"),
apiClient.getService("api").get("/users/notifications"),
apiClient.getService("api").get("/users/preferences"),
];
const responses = await Promise.all(promises);
// All calls will succeed after a single token refreshConfiguration
You can configure the token refresh behavior per service:
ApiConfig.initialize({
services: {
auth: {
name: "auth",
baseURL: "https://api.example.com/auth",
// Token refresh configuration
refreshEndpoint: "/refresh", // Custom refresh endpoint
refreshMethod: "post", // HTTP method for refresh
refreshRequestBody: {}, // Payload for refresh request
refreshTokenHeaderName: "Authorization", // Header name for refresh token
refreshTokenPrefix: "Bearer", // Prefix for refresh token
accessTokenPath: "data.access_token", // Path to access token in response
},
api: {
name: "api",
baseURL: "https://api.example.com/v1",
},
},
// Custom refresh handler (optional)
onRefreshRequest: async (refreshToken, axiosLib, services) => {
// Your custom refresh logic
const response = await axiosLib.post("/custom/refresh", {
refresh_token: refreshToken,
});
return response.data.access_token;
},
});API Reference
Hooks
useApiQuery
const { data, isLoading, error, refetch } = useApiQuery({
serviceName: "api", // Required: Service name
key: ["users"], // Required: Query key
url: "/users", // Required: API endpoint
enabled: true, // Optional: Enable/disable query
method: "get", // Optional: HTTP method (default: 'get')
params: { page: 1 }, // Optional: Query parameters
data: { filter: "active" }, // Optional: Request body (for POST)
config: {
// Optional: Axios config
headers: { "X-Custom": "value" },
},
options: {
// Optional: React Query options
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
},
});useApiMutation
const mutation = useApiMutation({
serviceName: "api", // Required: Service name
url: "/users", // Required: API endpoint
method: "post", // Optional: HTTP method (default: 'post')
keyToInvalidate: ["users"], // Optional: Queries to invalidate on success
successMessage: "User created successfully!", // Optional: Success message
errorMessage: "Failed to create user", // Optional: Custom error message
config: {
// Optional: Axios config
headers: { "X-Custom": "value" },
},
options: {
// Optional: React Query mutation options
onSuccess: (data) => {
console.log("Success:", data);
},
onError: (error) => {
console.error("Error:", error);
},
},
});
// Use the mutation
mutation.mutate({ name: "John Doe", email: "[email protected]" });Service-Specific Hooks
import { createUseQuery, createUseMutation } from "@learningpad/api-client";
// Create service-specific hooks
const useAuthQuery = createUseQuery("auth");
const useAuthMutation = createUseMutation("auth");
// Use them
const { data: user } = useAuthQuery({
key: ["user"],
url: "/me",
});
const login = useAuthMutation({
url: "/login",
method: "post",
});Direct API Methods
import { ApiService } from "@learningpad/api-client";
const apiService = new ApiService("api");
// Direct API calls (outside React components)
const users = await apiService.get("/users");
const newUser = await apiService.post("/users", { name: "John" });
const updatedUser = await apiService.put("/users/1", { name: "Jane" });
const deleted = await apiService.delete("/users/1");Error Handling
The API client provides comprehensive error handling:
import {
getErrorMessage,
isAxiosError,
getErrorStatus,
} from "@learningpad/api-client";
const { data, error } = useApiQuery({
serviceName: "api",
key: ["users"],
url: "/users",
});
if (error) {
if (isAxiosError(error)) {
const status = getErrorStatus(error);
const message = getErrorMessage(error);
if (status === 404) {
console.log("Resource not found");
} else if (status === 500) {
console.log("Server error");
}
}
}Utilities
Query Key Helpers
import { createQueryKey } from "@learningpad/api-client";
// Create consistent query keys
const userKey = createQueryKey("user", userId);
const usersKey = createQueryKey("users", { page, limit });Retry Configuration
import { createRetryConfig } from "@learningpad/api-client";
const retryConfig = createRetryConfig(3, 1000); // 3 retries, 1s base delay
const { data } = useApiQuery({
serviceName: "api",
key: ["data"],
url: "/data",
options: retryConfig,
});Cache Configuration
import { createCacheConfig } from "@learningpad/api-client";
const cacheConfig = createCacheConfig(
5 * 60 * 1000, // 5 minutes stale time
10 * 60 * 1000 // 10 minutes cache time
);
const { data } = useApiQuery({
serviceName: "api",
key: ["data"],
url: "/data",
options: cacheConfig,
});TypeScript Support
The package is fully typed with comprehensive TypeScript definitions:
import {
UseQueryApiProps,
UseMutationApiProps,
ApiError,
} from "@learningpad/api-client";
interface User {
id: string;
name: string;
email: string;
}
interface CreateUserRequest {
name: string;
email: string;
}
// Typed query
const { data: users } = useApiQuery<User[]>({
serviceName: "api",
key: ["users"],
url: "/users",
});
// Typed mutation
const createUser = useApiMutation<User, CreateUserRequest>({
serviceName: "api",
url: "/users",
method: "post",
});Examples
Complete Example with Authentication
import React from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import {
ApiConfig,
useApiQuery,
useApiMutation,
} from "@learningpad/api-client";
// Initialize API client
ApiConfig.initialize({
services: {
auth: {
name: "auth",
baseURL: "https://auth.example.com",
},
api: {
name: "api",
baseURL: "https://api.example.com/v1",
},
},
onUnauthorized: () => {
window.location.href = "/login";
},
});
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<UserDashboard />
</QueryClientProvider>
);
}
function UserDashboard() {
const { data: user, isLoading } = useApiQuery({
serviceName: "api",
key: ["user"],
url: "/user/profile",
});
const updateProfile = useApiMutation({
serviceName: "api",
url: "/user/profile",
method: "put",
keyToInvalidate: ["user"],
successMessage: "Profile updated successfully!",
});
if (isLoading) return <div>Loading...</div>;
return (
<div>
<h1>Welcome, {user?.name}!</h1>
<button
onClick={() => updateProfile.mutate({ name: "New Name" })}
disabled={updateProfile.isPending}
>
{updateProfile.isPending ? "Updating..." : "Update Profile"}
</button>
</div>
);
}Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
MIT © LearningPad Team
Support
For support, email [email protected] or join our Slack channel.
