
Security News
Attackers Are Hunting High-Impact Node.js Maintainers in a Coordinated Social Engineering Campaign
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.
learningpad-api-client
Advanced tools
A powerful, type-safe API client built on top of React Query and Axios with automatic token management and error handling
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.
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 axios
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>
);
}
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";
},
});
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>
);
}
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",
},
});
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);
},
});
The API client includes an intelligent retry mechanism that prevents multiple token refresh requests when multiple APIs fail with 401 errors simultaneously.
// 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 refresh
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;
},
});
useApiQueryconst { 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
},
});
useApiMutationconst 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: "john@example.com" });
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",
});
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");
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");
}
}
}
import { createQueryKey } from "@learningpad/api-client";
// Create consistent query keys
const userKey = createQueryKey("user", userId);
const usersKey = createQueryKey("users", { page, limit });
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,
});
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,
});
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",
});
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>
);
}
Contributions are welcome! Please feel free to submit a Pull Request.
MIT © LearningPad Team
For support, email support@learningpad.com or join our Slack channel.
FAQs
A powerful, type-safe API client built on top of React Query and Axios with automatic token management and error handling
We found that learningpad-api-client demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.