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

@mehdashti/data-client

v0.4.2

Published

TanStack Query adapter with error mapping and retry logic for Smart Platform

Readme

@smart/data-client

TanStack Query adapter with error mapping and retry logic for Smart Platform

Installation

pnpm add @smart/data-client @tanstack/react-query

Features

  • Type-safe Query Keys: Factory functions for consistent query key patterns
  • Error Mapping: Automatic mapping from @smart/contracts ErrorResponse
  • Retry Logic: Smart retry behavior (5xx, 429, network errors)
  • Custom Hooks: useApiQuery, useApiMutation, usePaginatedQuery
  • Query Client: Pre-configured QueryClient with platform defaults

Quick Start

1. Setup QueryClient

import { QueryClientProvider } from "@tanstack/react-query";
import { createQueryClient } from "@smart/data-client";

const queryClient = createQueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <YourApp />
    </QueryClientProvider>
  );
}

2. Use in Components

import { useApiQuery, apiFetch, queryKeys } from "@smart/data-client";

function UserProfile({ userId }: { userId: string }) {
  const { data, error, isLoading } = useApiQuery({
    queryKey: queryKeys.users.detail(userId),
    queryFn: () => apiFetch(`/api/users/${userId}`),
  });

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return <div>{data.name}</div>;
}

Query Keys

Type-safe query key factories for consistent patterns:

import { createQueryKeyFactory, queryKeys } from "@smart/data-client";

// Built-in factories
queryKeys.users.all; // ["users"]
queryKeys.users.lists(); // ["users", "list"]
queryKeys.users.list({ role: "admin" }); // ["users", "list", { role: "admin" }]
queryKeys.users.details(); // ["users", "detail"]
queryKeys.users.detail(123); // ["users", "detail", 123]

// Create custom factory
const productKeys = createQueryKeyFactory("products");
productKeys.all; // ["products"]
productKeys.list({ category: "electronics" }); // ["products", "list", { category: "electronics" }]

Hooks

useApiQuery

Wrapper around useQuery with automatic error mapping:

import { useApiQuery, apiFetch } from "@smart/data-client";

const { data, error, isLoading } = useApiQuery({
  queryKey: ["users", userId],
  queryFn: () => apiFetch<User>(`/api/users/${userId}`),
});

// error is automatically typed as ApiError
if (error) {
  console.log(error.response.detail); // ErrorResponse detail
  console.log(error.status); // HTTP status code
}

useApiMutation

Wrapper around useMutation with automatic error mapping:

import { useApiMutation, apiMutate } from "@smart/data-client";
import { useQueryClient } from "@tanstack/react-query";

const queryClient = useQueryClient();

const mutation = useApiMutation({
  mutationFn: (data: CreateUserData) =>
    apiMutate<User, CreateUserData>("/api/users", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(data),
    }),
  onSuccess: () => {
    queryClient.invalidateQueries({ queryKey: ["users"] });
  },
});

// Use the mutation
mutation.mutate({ name: "John", email: "[email protected]" });

usePaginatedQuery

Helper for paginated queries with Smart Platform pagination contract:

import { usePaginatedQuery } from "@smart/data-client";

const { data, isLoading } = usePaginatedQuery<User>({
  queryKey: ["users", { page, page_size }],
  endpoint: "/api/users",
  params: {
    page,
    page_size: 20,
    role: "admin",
  },
});

// data.data contains the array of users
// data.meta contains pagination metadata
console.log(data.data); // User[]
console.log(data.meta); // { page, page_size, total_items, total_pages }

Error Mapping

Utilities for working with API errors:

import {
  isApiError,
  getErrorMessage,
  getFieldErrors,
  getFieldError,
  isValidationError,
  isAuthError,
} from "@smart/data-client";

// Check error type
if (isApiError(error)) {
  console.log(error.response.type); // "validation_error", "not_found", etc.
}

// Get error message
const message = getErrorMessage(error); // Works with any error type

// Handle validation errors
if (isValidationError(error)) {
  const fieldErrors = getFieldErrors(error);
  fieldErrors.forEach((e) => {
    console.log(`${e.field}: ${e.message}`);
  });

  // Get specific field error
  const emailError = getFieldError(error, "email");
}

// Check specific error types
if (isAuthError(error)) {
  // Redirect to login
}

Retry Logic

Automatic retry with exponential backoff:

  • Retries: Network errors, 5xx server errors, 429 rate limit
  • No retry: 4xx client errors, auth errors, validation errors
  • Max retries: 3 attempts
  • Backoff: 1s, 2s, 4s (exponential)
import { shouldRetry, getRetryDelay, retryConfig } from "@smart/data-client";

// Use in custom QueryClient
import { QueryClient } from "@tanstack/react-query";

const queryClient = new QueryClient({
  defaultOptions: {
    queries: retryConfig,
  },
});

// Or customize
const customClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: (failureCount, error) => {
        // Custom logic
        if (failureCount > 5) return false;
        return shouldRetry(failureCount, error);
      },
      retryDelay: getRetryDelay,
    },
  },
});

Query Client Configuration

The createQueryClient function provides smart defaults:

import { createQueryClient } from "@smart/data-client";

const queryClient = createQueryClient({
  // Override defaults
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000, // 5 minutes instead of 1
    },
  },
});

Default configuration:

  • staleTime: 1 minute
  • gcTime: 5 minutes (garbage collection)
  • refetchOnWindowFocus: false
  • retry: Smart retry logic (5xx, 429, network)
  • retryDelay: Exponential backoff (1s, 2s, 4s)

Examples

Fetching a List

import { useApiQuery, apiFetch, queryKeys } from "@smart/data-client";

function UserList() {
  const { data, error, isLoading } = useApiQuery({
    queryKey: queryKeys.users.list(),
    queryFn: () => apiFetch<User[]>("/api/users"),
  });

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

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

Creating a Resource

import { useApiMutation, apiMutate } from "@smart/data-client";
import { useQueryClient } from "@tanstack/react-query";

function CreateUserForm() {
  const queryClient = useQueryClient();

  const mutation = useApiMutation({
    mutationFn: (data: CreateUserData) =>
      apiMutate<User>("/api/users", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(data),
      }),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["users"] });
    },
  });

  const handleSubmit = (data: CreateUserData) => {
    mutation.mutate(data);
  };

  return (
    <form onSubmit={(e) => {
      e.preventDefault();
      handleSubmit({ name: "John", email: "[email protected]" });
    }}>
      {mutation.error && (
        <div className="error">
          {getErrorMessage(mutation.error)}
        </div>
      )}
      <button type="submit" disabled={mutation.isPending}>
        Create User
      </button>
    </form>
  );
}

Pagination with Filters

import { usePaginatedQuery } from "@smart/data-client";
import { useState } from "react";

function UserTable() {
  const [page, setPage] = useState(1);
  const [role, setRole] = useState<string | undefined>();

  const { data, isLoading } = usePaginatedQuery<User>({
    queryKey: ["users", { page, role }],
    endpoint: "/api/users",
    params: {
      page,
      page_size: 20,
      role,
    },
  });

  if (isLoading) return <div>Loading...</div>;

  return (
    <div>
      <select onChange={(e) => setRole(e.target.value || undefined)}>
        <option value="">All roles</option>
        <option value="admin">Admin</option>
        <option value="viewer">Viewer</option>
      </select>

      <table>
        <tbody>
          {data.data.map((user) => (
            <tr key={user.id}>
              <td>{user.name}</td>
              <td>{user.email}</td>
            </tr>
          ))}
        </tbody>
      </table>

      <div>
        <button
          onClick={() => setPage(page - 1)}
          disabled={page === 1}
        >
          Previous
        </button>
        <span>Page {data.meta.page} of {data.meta.total_pages}</span>
        <button
          onClick={() => setPage(page + 1)}
          disabled={page >= data.meta.total_pages}
        >
          Next
        </button>
      </div>
    </div>
  );
}

Correlation IDs & Request Tracing

Correlation IDs allow you to trace related requests across frontend and backend services. All API calls automatically include a correlation ID in the X-Correlation-Id header.

Basic Usage (Automatic)

By default, each API call gets a unique correlation ID:

import { apiFetch } from "@smart/data-client";

// Each call gets a unique correlation ID
await apiFetch("/api/users/123");
await apiFetch("/api/posts/456");
// ☝️ These have different correlation IDs

Context-Aware Correlation (Recommended)

For better tracing, use CorrelationProvider to share correlation IDs across related requests:

import { CorrelationProvider } from "@smart/data-client";

function App() {
  return (
    <CorrelationProvider>
      <QueryClientProvider client={queryClient}>
        <YourApp />
      </QueryClientProvider>
    </CorrelationProvider>
  );
}

Using Correlation Scopes

Option 1: Manual Scope Control

import { useCorrelation } from "@smart/data-client";

function SaveUserButton({ userData }) {
  const correlation = useCorrelation();

  const handleSave = async () => {
    // Begin correlation scope
    correlation?.beginCorrelation();

    try {
      // All these requests share the same correlation ID
      await createUser(userData);
      await uploadAvatar(userData.avatar);
      await sendWelcomeEmail(userData.email);
    } finally {
      // End correlation scope
      correlation?.endCorrelation();
    }
  };

  return <button onClick={handleSave}>Save User</button>;
}

Option 2: Automatic Scope (Recommended)

import { useCorrelationScope } from "@smart/data-client";

function SaveUserButton({ userData }) {
  const withCorrelation = useCorrelationScope();

  // Automatically manages correlation scope
  const handleSave = withCorrelation(async (data) => {
    // All these requests share the same correlation ID
    await createUser(data);
    await uploadAvatar(data.avatar);
    await sendWelcomeEmail(data.email);
  });

  return <button onClick={() => handleSave(userData)}>Save User</button>;
}

Benefits of Correlation Scopes

  1. Better Debugging: Trace all related requests in your logs
  2. Error Tracking: Group related errors together
  3. Performance Monitoring: Measure end-to-end operation time
  4. Distributed Tracing: Track requests across microservices

Example: Form Submission with Correlation

import { useCorrelationScope, useApiMutation } from "@smart/data-client";
import { useSmartForm } from "@mehdashti/forms";

function CreateOrderForm() {
  const withCorrelation = useCorrelationScope();

  const form = useSmartForm({
    schema: orderSchema,
    onSubmit: withCorrelation(async (data) => {
      // These requests share the same correlation ID:
      const order = await createOrder(data);
      await processPayment(order.id, data.payment);
      await sendOrderConfirmation(order.id);
      await updateInventory(data.items);
      // In logs, you can trace all 4 requests together
    }),
  });

  return <form>...</form>;
}

Without CorrelationProvider

If you don't wrap your app in CorrelationProvider, the system falls back to generating a unique ID for each request (backward compatible behavior).

Request Timeout & Cancellation

Control request timeouts and cancel requests using AbortSignal.

Timeout

Set a timeout for requests that should not exceed a certain duration:

import { apiFetch } from "@smart/data-client";

// Abort after 5 seconds
const user = await apiFetch<User>("/api/users/123", {
  timeout: 5000
});

Manual Cancellation

Cancel requests manually using AbortController:

import { apiFetch } from "@smart/data-client";

const controller = new AbortController();

// Start request
const promise = apiFetch<User>("/api/users/123", {
  signal: controller.signal
});

// Cancel it
controller.abort();

try {
  await promise;
} catch (error) {
  console.log("Request was aborted");
}

Timeout + Manual Cancellation

Combine timeout with manual cancellation:

import { apiFetch } from "@smart/data-client";

const controller = new AbortController();

const user = await apiFetch<User>("/api/users/123", {
  timeout: 10000, // Auto-abort after 10 seconds
  signal: controller.signal // Also allow manual abort
});

// Can still manually abort before timeout
controller.abort();

Utilities

import { createTimeoutSignal, combineAbortSignals } from "@smart/data-client";

// Create a timeout signal
const timeoutSignal = createTimeoutSignal(5000);
await apiFetch("/api/users", { signal: timeoutSignal });

// Combine multiple signals
const userSignal = new AbortController().signal;
const timeoutSignal = createTimeoutSignal(10000);
const combined = combineAbortSignals([userSignal, timeoutSignal]);
await apiFetch("/api/users", { signal: combined });

With TanStack Query

TanStack Query automatically provides an AbortSignal for query cancellation:

import { useApiQuery, apiFetch } from "@smart/data-client";

const { data } = useApiQuery({
  queryKey: ["users", userId],
  queryFn: ({ signal }) => apiFetch(`/api/users/${userId}`, {
    signal, // TanStack Query's signal
    timeout: 5000 // Add timeout
  })
});

Type Safety

All exports are fully typed for TypeScript:

import type {
  ApiError,
  QueryKeyFactory,
  PaginationParams,
  SortParams,
  FilterParams,
  QueryParams,
} from "@smart/data-client";

License

MIT