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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@iamankan/react-api-client

v0.1.1

Published

A typed API client factory for defining and calling HTTP endpoints

Downloads

272

Readme

@iamankan/react-api-client

A typed API client factory for defining and calling HTTP endpoints with full TypeScript type safety, built on top of Axios.

npm version license


Overview

@iamankan/react-api-client lets you define your API surface once — with full request/response/parameter types — and then consume it anywhere in your app with zero runtime overhead and complete IDE autocomplete.

Why use this library?

  • Type-safe by default — TypeScript infers request body shapes, query params, and response types from your definitions
  • No code generation — types are defined inline with your endpoints
  • Composable — separate concerns cleanly: define endpoints, configure the HTTP client, assemble the API
  • Built on Axios — supports all Axios config options (headers, interceptors, signal, etc.)
  • Tiny footprint — no runtime schema validation, no magic

Installation

npm install @iamankan/react-api-client axios
# or
yarn add @iamankan/react-api-client axios
# or
pnpm add @iamankan/react-api-client axios

Note: axios is a peer dependency and must be installed separately.


Quick Start

import { defineEndpoint, createApiClient, createApi } from "@iamankan/react-api-client";

// 1. Define your types
type User = { id: string; name: string; email: string };

// 2. Define endpoints
const endpoints = {
  users: {
    list: defineEndpoint<User[]>("/users"),
    create: defineEndpoint<User, { name: string; email: string }>("/users", { method: "POST" }),
  },
};

// 3. Create the HTTP caller
const httpCaller = createApiClient({
  baseURL: "https://api.example.com/v1",
});

// 4. Assemble the API client
export const api = createApi(endpoints, httpCaller);

// 5. Call endpoints — fully typed
const users = await api.users.list();
// users: User[]

const newUser = await api.users.create({ data: { name: "Alice", email: "[email protected]" } });
// newUser: User

Core Concepts

The library follows a three-step factory pattern:

defineEndpoint  →  createApiClient  →  createApi
(what to call)     (how to call it)    (assembled client)

| Step | Function | Role | |---|---|---| | 1 | defineEndpoint | Declares a URL, HTTP method, and TypeScript types for an endpoint | | 2 | createApiClient | Creates an Axios instance with your base URL and default config | | 3 | createApi | Combines your endpoint definitions and HTTP caller into a callable API object |


API Reference

defineEndpoint

Declares a typed endpoint. This function does not make any network calls — it only stores the URL and method and carries your type information.

Signature

function defineEndpoint<TResponse, TBody = never, TParams = never>(
  url: string,
  options?: { method?: Method },
): ApiEndpoint<TResponse, TBody, TParams>

Type Parameters

| Parameter | Default | Description | |---|---|---| | TResponse | (required) | Shape of the successful response data | | TBody | never | Shape of the request body (for POST/PUT/PATCH) | | TParams | never | Shape of query/path parameters |

Parameters

| Parameter | Type | Required | Description | |---|---|---|---| | url | string | Yes | Endpoint path (e.g. /users, /users/:id) | | options.method | Method | No | HTTP method — defaults to "GET" |

Examples

// GET with no body or params — response type only
const listPosts = defineEndpoint<Post[]>("/posts");

// POST with a typed request body
const createPost = defineEndpoint<Post, { title: string; body: string }>(
  "/posts",
  { method: "POST" }
);

// GET with query/path parameters
const getPost = defineEndpoint<Post, never, { id: string }>("/posts/:id");

// PUT with both body and params
const updatePost = defineEndpoint<Post, { title: string }, { id: string }>(
  "/posts/:id",
  { method: "PUT" }
);

// DELETE with params, void response
const deletePost = defineEndpoint<void, never, { id: string }>(
  "/posts/:id",
  { method: "DELETE" }
);

createApiClient

Creates an Axios-based HTTP caller function with default configuration.

Signature

function createApiClient(config: ApiClientConfig): HttpCaller

ApiClientConfig

| Option | Type | Default | Description | |---|---|---|---| | baseURL | string | (required) | Base URL prepended to all endpoint URLs | | onUnauthorized | () => void | undefined | Called when any request receives a 401 response | | withCredentials | boolean | true | Include cookies/credentials in cross-origin requests | | timeout | number | 15000 | Request timeout in milliseconds | | headers | object | { "Content-Type": "application/json" } | Default request headers | | any | any | — | Any other AxiosRequestConfig option |

User-supplied values override the defaults above.

Examples

// Minimal setup
const httpCaller = createApiClient({
  baseURL: "https://api.example.com/v1",
});

// With auth handling
const httpCaller = createApiClient({
  baseURL: "https://api.example.com/v1",
  onUnauthorized: () => {
    localStorage.removeItem("token");
    window.location.href = "/login";
  },
});

// Custom timeout and headers
const httpCaller = createApiClient({
  baseURL: "https://api.example.com/v1",
  timeout: 30000,
  headers: {
    "Content-Type": "application/json",
    "X-API-Version": "2",
  },
});

// Disable credentials for public APIs
const httpCaller = createApiClient({
  baseURL: "https://api.example.com/v1",
  withCredentials: false,
});

createApi

Assembles endpoint definitions and an HTTP caller into a fully-typed, callable API client object.

Signature

function createApi<TEndpoints extends EndpointGroups>(
  endpoints: TEndpoints,
  caller: HttpCaller,
): ApiFromEndpoints<TEndpoints>

Parameters

| Parameter | Type | Description | |---|---|---| | endpoints | EndpointGroups | Nested object mapping group names to endpoint definitions | | caller | HttpCaller | HTTP caller from createApiClient |

Endpoint groups structure

const endpoints = {
  // group name
  users: {
    // endpoint name: endpoint definition
    list: defineEndpoint<User[]>("/users"),
    create: defineEndpoint<User, CreateUserInput>("/users", { method: "POST" }),
  },
  posts: {
    getById: defineEndpoint<Post, never, { id: string }>("/posts/:id"),
  },
};

Calling endpoints

Each assembled endpoint is a function with this call signature:

api.group.endpoint(options?) => Promise<TResponse>

The options object is typed based on the endpoint definition:

| Option | Type | When available | |---|---|---| | data | TBody | When TBody is defined (not never) | | params | TParams | When TParams is defined (not never) | | any | any | Any AxiosRequestConfig option except url |

// No options needed (GET with no params)
const users = await api.users.list();

// With request body
const user = await api.users.create({
  data: { name: "Alice", email: "[email protected]" },
});

// With path/query params
const post = await api.posts.getById({ params: { id: "123" } });

// With extra Axios config (e.g. AbortController)
const controller = new AbortController();
const users = await api.users.list({ signal: controller.signal });

ApiError

Custom error class thrown whenever a request fails. All Axios errors are normalized to ApiError before being re-thrown — you will never see a raw Axios error.

Class definition

class ApiError extends Error {
  readonly name: "ApiError";
  readonly status: number;     // HTTP status code, or 500 for network/unexpected errors
  readonly details?: unknown;  // Raw response.data from the server
}

Error normalization rules

| Scenario | status | message | details | |---|---|---|---| | HTTP error response | response.status | response.data.message or Axios error message | response.data | | Network error / timeout | 500 | Axios error message | undefined | | Non-Axios error | 500 | "Unexpected error" | The original error |

Usage

import { ApiError } from "@iamankan/react-api-client";

try {
  const user = await api.users.getById({ params: { id: "not-found" } });
} catch (error) {
  if (error instanceof ApiError) {
    console.error(`[${error.status}] ${error.message}`);
    // e.g. "[404] User not found"

    if (error.status === 404) {
      // Handle not found
    } else if (error.status >= 500) {
      // Handle server error
    }
  }
}

TypeScript Types Reference

All types are exported from @iamankan/react-api-client and can be imported directly.

| Type | Description | |---|---| | ApiEndpoint<TResponse, TBody, TParams> | The return type of defineEndpoint | | ApiClientConfig | Config object accepted by createApiClient | | HttpCaller | The function type returned by createApiClient | | EndpointGroups | Type constraint for the endpoints argument of createApi | | ApiFromEndpoints<TEndpoints> | The type of the assembled API client returned by createApi | | ApiCallOptions<TEndpoint> | Options accepted when calling an assembled endpoint method | | EndpointResponse<TEndpoint> | Extracts TResponse from an ApiEndpoint type | | EndpointBody<TEndpoint> | Extracts TBody from an ApiEndpoint type | | EndpointParams<TEndpoint> | Extracts TParams from an ApiEndpoint type |

Using utility types to stay DRY

import type {
  EndpointResponse,
  EndpointBody,
  EndpointParams,
} from "@iamankan/react-api-client";

const createUser = defineEndpoint<User, { name: string; email: string }>(
  "/users",
  { method: "POST" }
);

// Derive types from the endpoint definition instead of repeating them
type CreateUserResponse = EndpointResponse<typeof createUser>; // User
type CreateUserBody = EndpointBody<typeof createUser>;         // { name: string; email: string }

Full Integration Example

A complete Users CRUD implementation showing real-world usage patterns.

src/api/endpoints/users.ts — define endpoints alongside their domain types

import { defineEndpoint } from "@iamankan/react-api-client";

export type User = {
  id: string;
  name: string;
  email: string;
  createdAt: string;
};

export type CreateUserInput = Pick<User, "name" | "email">;
export type UpdateUserInput = Partial<CreateUserInput>;

export const userEndpoints = {
  list:     defineEndpoint<User[]>("/users"),
  getById:  defineEndpoint<User, never, { id: string }>("/users/:id"),
  create:   defineEndpoint<User, CreateUserInput>("/users", { method: "POST" }),
  update:   defineEndpoint<User, UpdateUserInput, { id: string }>("/users/:id", { method: "PATCH" }),
  remove:   defineEndpoint<void, never, { id: string }>("/users/:id", { method: "DELETE" }),
};

src/api/index.ts — assemble and export a single shared client

import { createApiClient, createApi } from "@iamankan/react-api-client";
import { userEndpoints } from "./endpoints/users";

const httpCaller = createApiClient({
  baseURL: import.meta.env.VITE_API_BASE_URL,
  onUnauthorized: () => {
    window.location.href = "/login";
  },
});

export const api = createApi({ users: userEndpoints }, httpCaller);

src/components/UserList.tsx — use in a React component

import { useState, useEffect } from "react";
import { ApiError } from "@iamankan/react-api-client";
import { api } from "../api";
import type { User } from "../api/endpoints/users";

export function UserList() {
  const [users, setUsers] = useState<User[]>([]);
  const [error, setError] = useState<string | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const controller = new AbortController();

    api.users.list({ signal: controller.signal })
      .then(setUsers)
      .catch((err) => {
        if (err instanceof ApiError) {
          setError(`Failed to load users (${err.status}): ${err.message}`);
        }
      })
      .finally(() => setLoading(false));

    return () => controller.abort();
  }, []);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>{error}</p>;

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name} — {user.email}</li>
      ))}
    </ul>
  );
}

src/components/CreateUser.tsx — mutation example

import { useState } from "react";
import { ApiError } from "@iamankan/react-api-client";
import { api } from "../api";

export function CreateUser() {
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    try {
      const user = await api.users.create({ data: { name, email } });
      console.log("Created:", user);
    } catch (err) {
      if (err instanceof ApiError) {
        alert(`Error ${err.status}: ${err.message}`);
      }
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input value={name} onChange={(e) => setName(e.target.value)} placeholder="Name" />
      <input value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" />
      <button type="submit">Create</button>
    </form>
  );
}

Best Practices

1. Co-locate endpoint definitions with their domain

Keep endpoint definitions next to the types and components they serve. This makes it easy to update types and endpoints together when your API changes.

src/
  api/
    endpoints/
      users.ts      ← User type + userEndpoints
      posts.ts      ← Post type + postEndpoints
    index.ts        ← single exported `api` instance

2. Export one shared api instance

Create a single api object and import it wherever you need it. Avoid creating multiple createApi or createApiClient calls — you'd create multiple Axios instances with separate interceptor chains.

// src/api/index.ts
export const api = createApi(endpoints, httpCaller);

// anywhere else
import { api } from "../api";

3. Use utility types to avoid duplicating type definitions

Instead of re-declaring User or CreateUserInput in multiple places, derive them from the endpoint:

import type { EndpointBody, EndpointResponse } from "@iamankan/react-api-client";
import { userEndpoints } from "./endpoints/users";

type UserListResponse = EndpointResponse<typeof userEndpoints.list>;     // User[]
type CreateUserBody   = EndpointBody<typeof userEndpoints.create>;       // CreateUserInput

4. Centralize onUnauthorized for auth token management

The onUnauthorized callback fires on every 401 response. Use it to clear stored tokens and redirect to your login page:

const httpCaller = createApiClient({
  baseURL: "https://api.example.com",
  onUnauthorized: () => {
    // Clear auth state
    authStore.clearToken();
    // Redirect
    router.push("/login");
  },
});

5. Group endpoints by resource/domain

Structure endpoint groups to mirror your API's resource model. This keeps the assembled API intuitive to use:

const endpoints = {
  auth:    { login, logout, refresh },
  users:   { list, getById, create, update, remove },
  posts:   { list, getById, create, publish },
  comments: { list, create, remove },
};

// Usage reads naturally
await api.auth.login({ data: credentials });
await api.posts.publish({ params: { id: "123" } });

6. Cancel requests when components unmount

Pass an AbortController signal via options to avoid state updates on unmounted components:

useEffect(() => {
  const controller = new AbortController();
  api.users.list({ signal: controller.signal }).then(setUsers);
  return () => controller.abort();
}, []);

Error Handling Guide

Basic pattern

import { ApiError } from "@iamankan/react-api-client";

try {
  const data = await api.users.list();
} catch (error) {
  if (error instanceof ApiError) {
    switch (error.status) {
      case 400: console.error("Bad request:", error.details); break;
      case 401: /* handled by onUnauthorized */ break;
      case 403: console.error("Forbidden"); break;
      case 404: console.error("Not found"); break;
      default:  console.error("Server error:", error.message);
    }
  }
}

Async/await with error state in React

const [error, setError] = useState<ApiError | null>(null);

async function loadUser(id: string) {
  try {
    const user = await api.users.getById({ params: { id } });
    setUser(user);
  } catch (err) {
    if (err instanceof ApiError) setError(err);
  }
}

// In JSX
{error && <ErrorBanner status={error.status} message={error.message} />}

Reading server-provided error details

When your API returns a structured error body, access it via error.details:

// Server responds with: { message: "Validation failed", fields: { email: "Invalid format" } }
catch (error) {
  if (error instanceof ApiError && error.status === 422) {
    const fields = (error.details as any)?.fields;
    // { email: "Invalid format" }
  }
}

Configuration Reference

Full list of options accepted by createApiClient:

| Option | Type | Default | Description | |---|---|---|---| | baseURL | string | (required) | Prepended to all endpoint URLs | | onUnauthorized | () => void | undefined | Fired on 401 responses before the error propagates | | withCredentials | boolean | true | Send cookies on cross-origin requests | | timeout | number | 15000 | Milliseconds before the request times out | | headers | RawAxiosHeaders | { "Content-Type": "application/json" } | Default headers on every request | | responseType | string | "json" | Expected response data type | | maxRedirects | number | 5 | Maximum number of redirects |

All other Axios request config options are also accepted and forwarded directly to the underlying Axios instance.


Build & Distribution

The package ships dual-format bundles for broad compatibility:

| Format | File | Use case | |---|---|---| | ES Modules | dist/index.js | Bundlers (Vite, webpack, Rollup) | | CommonJS | dist/index.cjs | Node.js require() | | TypeScript declarations | dist/index.d.ts / dist/index.d.cts | IDE and type checking |

Source maps are included for all formats.


License

MIT