npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

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

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 axios

Quick 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

  1. Multiple API calls fail with 401 - When 4 API calls fail with 401 at the same time
  2. Only one token refresh - Only the first call triggers a token refresh
  3. Other calls wait - The remaining 3 calls wait for the refresh to complete
  4. All calls retry - Once the new token is obtained, ALL calls retry with the new token
  5. 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 refresh

Configuration

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.