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

@rabstack/rab-react-sdk

v0.8.1

Published

React SDK for building applications with React Query and other utilities

Readme

@rabstack/rab-react-sdk

A generic React SDK library for building type-safe React Query hooks with any API SDK. This library provides reusable components and hooks that wrap your API SDK with React Query, giving you automatic loading states, caching, and error handling.

Features

  • Type-Safe - Full TypeScript support with automatic type inference from endpoint specifications
  • React Query Integration - Built-in hooks for queries and mutations
  • Spec-Based SDK Creation - Define your API endpoints as specifications and automatically generate type-safe SDK methods
  • Advanced Error Handling - Built-in ApiDomainError class with helper methods for common error scenarios
  • Global Error Handler - Optional callback to handle all API errors globally (logging, analytics, notifications, etc.)
  • Zero Configuration - Automatic SDK generation from endpoint specs with full type inference
  • Context-Based - SDK provided via React Context for easy access
  • Flexible & Extensible - Customizable error handling, response transformation, and query encoding

Installation

npm install @rabstack/rab-react-sdk @tanstack/react-query
# or
yarn add @rabstack/rab-react-sdk @tanstack/react-query
# or
pnpm add @rabstack/rab-react-sdk @tanstack/react-query

Quick Start

The SDK uses a spec-based approach where you define your API endpoints as specifications and the library automatically generates type-safe SDK methods and React Query hooks.

Spec-Based SDK

This approach automatically generates your SDK from endpoint specifications. It's simpler and requires less boilerplate.

Step 1: Define API Endpoint Specifications

// api-specs.ts
import { GetApi, PostApi, PutApi, DeleteApi } from '@rabstack/rab-react-sdk';

// Define your types
export interface Product {
  id: string;
  name: string;
  price: number;
}

export interface CreateProductDto {
  name: string;
  price: number;
}

// Define endpoint specifications using helper functions
export const donationApiSpecs = {
  // Query endpoints (GET)
  getProfile: GetApi<{ id: string; name: string }>('/profile'),

  listProducts: GetApi<Product[], { page?: number; pageSize?: number }>('/products'),

  getProduct: GetApi<Product, never, { productId: string }>('/products/:productId'),

  // Mutation endpoints (POST/PUT/DELETE)
  createProduct: PostApi<Product, CreateProductDto>('/products'),

  updateProduct: PutApi<Product, Partial<CreateProductDto>, { productId: string }>('/products/:productId'),

  deleteProduct: DeleteApi<void, { productId: string }>('/products/:productId'),
} as const;

Step 2: Create SDK Provider with Specs

// donation-sdk.tsx
'use client';

import { createSDKProvider } from '@rabstack/rab-react-sdk';
import { donationApiSpecs } from './api-specs';

export const {
  SDKProvider: DonationSDKProvider,
  useSDK: useDonationSDK,
  useQuery: useDonationQuery,
  useMutation: useDonationMutation,
} = createSDKProvider({
  apiSpecs: donationApiSpecs,
  // Optional: Define how to get authorization headers
  // getAuthorization: () => {
  //   const token = localStorage.getItem('token');
  //   return token ? { Authorization: `Bearer ${token}` } : undefined;
  // },
});

Step 3: Wrap Your App with SDK Provider

// app.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { DonationSDKProvider } from './donation-sdk';

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <DonationSDKProvider baseUrl="https://api.example.com">
        <YourApp />
      </DonationSDKProvider>
    </QueryClientProvider>
  );
}

Step 4: Use in Components

// components/ProductList.tsx
import { useDonationQuery, useDonationMutation } from './donation-sdk';

function ProductList() {
  // Query with parameters
  const { data: products, isLoading } = useDonationQuery('listProducts', {
    query: { page: 1, pageSize: 20 },
    enabled: true,
  });

  // Mutation with callbacks and automatic query invalidation
  const createProduct = useDonationMutation('createProduct', {
    onSuccess: (data) => {
      console.log('Product created:', data);
    },
    onError: (error) => {
      console.error('Failed to create product:', error);
    },
    // Automatically invalidate these queries when mutation completes
    invalidateQueries: ['listProducts'],
  });

  const handleCreate = () => {
    createProduct.mutate({
      body: {
        name: 'New Product',
        price: 100,
      },
    });
  };

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

  return (
    <div>
      <button onClick={handleCreate}>Create Product</button>
      {products?.map((product) => (
        <div key={product.id}>{product.name}</div>
      ))}
    </div>
  );
}

API Reference

Endpoint Specification Helpers

These helper functions provide a clean API for defining endpoint specifications:

GetApi<RESPONSE, QUERY?, PARAMS?>(url)

Create a GET endpoint specification.

import { GetApi } from '@rabstack/rab-react-sdk';

// Simple GET
const getProfile = GetApi<User>('/profile');

// GET with query parameters
const listUsers = GetApi<User[], { page?: number; limit?: number }>('/users');

// GET with path parameters
const getUser = GetApi<User, never, { userId: string }>('/users/:userId');

PostApi<RESPONSE, BODY, PARAMS?>(url)

Create a POST endpoint specification.

import { PostApi } from '@rabstack/rab-react-sdk';

// POST with body
const createUser = PostApi<User, CreateUserDto>('/users');

// POST with body and path parameters
const addComment = PostApi<Comment, CommentDto, { postId: string }>('/posts/:postId/comments');

PutApi<RESPONSE, BODY, PARAMS?>(url)

Create a PUT endpoint specification.

import { PutApi } from '@rabstack/rab-react-sdk';

const updateUser = PutApi<User, UpdateUserDto, { userId: string }>('/users/:userId');

PatchApi<RESPONSE, BODY, PARAMS?>(url)

Create a PATCH endpoint specification.

import { PatchApi } from '@rabstack/rab-react-sdk';

const partialUpdate = PatchApi<User, Partial<UserDto>, { userId: string }>('/users/:userId');

DeleteApi<RESPONSE?, PARAMS?>(url)

Create a DELETE endpoint specification.

import { DeleteApi } from '@rabstack/rab-react-sdk';

// DELETE with no response (void)
const deleteUser = DeleteApi<void, { userId: string }>('/users/:userId');

// DELETE with response
const softDelete = DeleteApi<User, { userId: string }>('/users/:userId');

Type Parameters Order:

  • RESPONSE - The response type (required)
  • BODY - Request body type (for POST/PUT/PATCH only, required)
  • QUERY - Query parameters type (for GET only, optional, use never to skip)
  • PARAMS - URL path parameters type (optional)

EndpointSpec<RESPONSE, BODY, QUERY, PARAMS>

The underlying type for endpoint specifications. Usually you'll use the helper functions above instead of this type directly.

Type Parameters:

  • RESPONSE - The response type returned by the endpoint
  • BODY - The request body type (use unknown if not applicable)
  • QUERY - The query parameters type (use unknown if not applicable)
  • PARAMS - The URL path parameters type (use unknown if not applicable)

Properties:

  • url: string - The endpoint URL path (supports :paramName placeholders)
  • method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' - The HTTP method

createSDKProvider<TApiSpecs>(config)

Creates a complete SDK setup with provider, hooks, and type-safe query/mutation hooks from API specifications.

Parameters:

  • config.apiSpecs - Object mapping endpoint names to EndpointSpec definitions
  • config.getAuthorization?: () => Record<string, string> | undefined - Optional function to provide authorization headers
  • config.extractApiError?: (status, statusText, url, method, data) => ApiErrorDetails - Optional custom error extractor
  • config.parseQuery?: (query: any) => string - Optional custom query string parser
  • config.transformResponse?: <T>(data: any, response: Response) => T - Optional response transformer (defaults to unwrapping { data: ... })
  • config.onError?: (error: ApiDomainError) => void - Optional global error handler called for all API errors (useful for logging, analytics, or notifications)

Returns:

  • SDKProvider - Provider component that accepts baseUrl, optional getAuthorization, and optional onError props
  • useSDK - Hook to access the raw SDK instance
  • useQuery - Type-safe query hook for GET requests
  • useMutation - Type-safe mutation hook for POST/PUT/DELETE requests
  • SDKContext - The React context (rarely needed directly)

Example:

import { createSDKProvider, GetApi, PostApi } from '@rabstack/rab-react-sdk';

const apiSpecs = {
  getUser: GetApi<User, never, { userId: string }>('/users/:userId'),
  createUser: PostApi<User, CreateUserDto>('/users'),
};

const { SDKProvider, useQuery, useMutation } = createSDKProvider({
  apiSpecs,
  // Optional: Provide authorization headers
  getAuthorization: () => {
    const token = getYourToken(); // Your token retrieval logic
    return token ? { Authorization: token } : undefined;
  },
});

// In your app
<SDKProvider baseUrl="https://api.example.com">
  <App />
</SDKProvider>

// In components
const { data } = useQuery('getUser', { params: { userId: '123' } });
const createUser = useMutation('createUser');

createSDK<TApiSpecs>(config)

Low-level function to create an SDK instance from API specifications. Usually you'll use createSDKProvider instead.

Parameters:

  • config.apiSpecs - Object mapping endpoint names to EndpointSpec definitions
  • config.baseUrl - The base URL for all API requests
  • config.getAuthorization?: () => Record<string, string> | undefined - Optional function to provide authorization headers
  • config.extractApiError? - Optional custom error extractor
  • config.parseQuery? - Optional custom query string parser

Returns: SDK object with type-safe methods

Example:

import { createSDK, GetApi } from '@rabstack/rab-react-sdk';

const sdk = createSDK({
  apiSpecs: {
    getUser: GetApi<User, never, { userId: string }>('/users/:userId'),
  },
  baseUrl: 'https://api.example.com',
});

const user = await sdk.getUser({ params: { userId: '123' } });

ApiDomainError

Custom error class for API errors with helper methods.

Properties:

  • status: number - HTTP status code
  • url: string - Request URL
  • method: string - HTTP method
  • message: string - Error message
  • errorCode?: string | number - Optional error code from API
  • errors?: string[] - Optional array of validation errors
  • data?: any - Raw error data from API

Methods:

  • isNetworkError(): boolean - Returns true if status is 0 (network/connection error)
  • isClientError(): boolean - Returns true if status is 4xx
  • isServerError(): boolean - Returns true if status is 5xx
  • isUnauthorized(): boolean - Returns true if status is 401
  • isForbidden(): boolean - Returns true if status is 403
  • isNotFound(): boolean - Returns true if status is 404
  • isValidationError(): boolean - Returns true if status is 400 or 422
  • isConflict(): boolean - Returns true if status is 409
  • getAllMessages(): string[] - Returns all error messages including validation errors
  • getDisplayMessage(): string - Returns formatted message for display

Example:

import { ApiDomainError } from '@rabstack/rab-react-sdk';

try {
  await sdk.createUser({ body: userData });
} catch (error) {
  // All errors from the SDK are ApiDomainError instances
  if (error.isValidationError()) {
    console.error('Validation errors:', error.getAllMessages());
  } else if (error.isUnauthorized()) {
    // Redirect to login
  } else if (error.isNetworkError()) {
    console.error('Network error - check your connection');
  } else {
    console.error(error.getDisplayMessage());
  }
}

buildUrlPath(...segments)

Utility function to build URL paths with parameter replacement.

Parameters:

  • ...segments - Path segments, optionally ending with a replacements object

Returns: Constructed URL path string

Example:

import { buildUrlPath } from '@rabstack/rab-react-sdk';

buildUrlPath('/users/:userId', { userId: '123' }); // '/users/123'
buildUrlPath('/api', 'users', ':userId', { userId: '456' }); // '/api/users/456'
buildUrlPath('/products'); // '/products'

TypeScript Support

This library automatically infers types from your SDK methods. No need to manually define request/response types!

Automatic Type Inference

// SDK method signature
getProduct: (options: { params: { productId: string } }) => Promise<Product>

// Automatically inferred types
const { data } = useQuery('getProduct', {
  params: { productId: '123' }, // ✅ Type-safe params - REQUIRED
});
// data is typed as Product | undefined

Conditional Required Fields

The library uses advanced TypeScript to enforce required fields based on your SDK definition:

// SDK with required body
createProduct: (options: { body: CreateProductDto }) => Promise<Product>

// ✅ Correct - body is REQUIRED
const create = useMutation('createProduct');
create.mutate({ body: { name: 'Product' } });

// ❌ Error - body is missing
create.mutate({}); // TypeScript error!

// SDK with optional params
listProducts: (options?: { query?: { page?: number } }) => Promise<Product[]>

// ✅ Correct - everything is optional
const { data } = useQuery('listProducts');
const { data } = useQuery('listProducts', { query: { page: 1 } });

// SDK with required params
getProduct: (options: { params: { productId: string } }) => Promise<Product>

// ✅ Correct - params is REQUIRED
const { data } = useQuery('getProduct', {
  params: { productId: '123' }
});

// ❌ Error - params is missing
const { data } = useQuery('getProduct'); // TypeScript error!

How it works:

  • If your SDK method has body or params in a required position, the hook will enforce them
  • If your SDK method has them as optional (using ?:), the hook makes them optional too
  • query and headers are always optional
  • encodeQuery is always optional (used for special query encoding)

Advanced Usage

Authorization Patterns

The getAuthorization function allows you to define custom authorization headers. Here are common patterns:

Bearer Token:

getAuthorization: () => {
  const token = localStorage.getItem('token');
  return token ? { Authorization: `Bearer ${token}` } : undefined;
}

API Key:

getAuthorization: () => ({
  'X-API-Key': process.env.REACT_APP_API_KEY || '',
})

Basic Auth:

getAuthorization: () => {
  const credentials = btoa(`${username}:${password}`);
  return { Authorization: `Basic ${credentials}` };
}

Custom Headers:

getAuthorization: () => {
  const session = getSession();
  return session ? {
    'X-Session-ID': session.id,
    'X-User-ID': session.userId,
  } : undefined;
}

Dynamic Provider-Level Authorization:

export const { SDKProvider, useQuery, useMutation } = createSDKProvider({
  apiSpecs,
  // Optional default authorization
  getAuthorization: () => {
    const token = localStorage.getItem('token');
    return token ? { Authorization: `Bearer ${token}` } : undefined;
  },
});

// You can also override getAuthorization at the provider level
function App() {
  return (
    <SDKProvider
      baseUrl="https://api.example.com"
      getAuthorization={() => {
        // Override with different auth logic
        const apiKey = getApiKey();
        return apiKey ? { 'X-API-Key': apiKey } : undefined;
      }}
    >
      <YourApp />
    </SDKProvider>
  );
}

Error Handling with ApiDomainError

The SDK uses ApiDomainError for all API errors, providing rich error information and helper methods. All errors thrown by the SDK are guaranteed to be ApiDomainError instances, so you can use the helper methods directly without type checking:

import { useDonationQuery, useDonationMutation } from './donation-sdk';

function UserProfile() {
  const { data, error } = useDonationQuery('getProfile');

  // All errors from the SDK are ApiDomainError instances
  if (error) {
    if (error.isUnauthorized()) {
      return <div>Please log in to view your profile</div>;
    }

    if (error.isNotFound()) {
      return <div>Profile not found</div>;
    }

    if (error.isValidationError()) {
      return (
        <div>
          <h3>Validation Errors:</h3>
          <ul>
            {error.getAllMessages().map((msg, i) => (
              <li key={i}>{msg}</li>
            ))}
          </ul>
        </div>
      );
    }

    if (error.isNetworkError()) {
      return <div>Network error - check your connection</div>;
    }

    // Generic error display
    return <div>Error: {error.getDisplayMessage()}</div>;
  }

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

function CreateProduct() {
  const createProduct = useDonationMutation('createProduct', {
    onError: (error) => {
      // All errors from the SDK are ApiDomainError instances
      if (error.isConflict()) {
        alert('A product with this name already exists');
      } else if (error.isValidationError()) {
        alert(`Validation failed: ${error.getAllMessages().join(', ')}`);
      } else if (error.isServerError()) {
        alert('Server error. Please try again later.');
      } else if (error.isNetworkError()) {
        alert('Network error. Please check your connection.');
      } else {
        alert(error.getDisplayMessage());
      }
    },
  });

  // ...
}

Custom Error Extraction

You can customize how errors are extracted from API responses:

import { createSDKProvider, type ApiErrorDetails } from '@rabstack/rab-react-sdk';

function extractApiError(
  status: number,
  statusText: string,
  url: string,
  method: string,
  data?: any
): ApiErrorDetails {
  // Custom error extraction logic for your API format
  return {
    status,
    url,
    method,
    message: data?.error?.message || data?.message || statusText,
    errorCode: data?.error?.code || data?.code,
    errors: data?.error?.details || data?.errors || [],
    data,
  };
}

const { SDKProvider } = createSDKProvider({
  apiSpecs,
  extractApiError, // Use custom error extractor
});

Global Error Handler

The onError callback provides a centralized place to handle all API errors globally. This is useful for logging, analytics, displaying notifications, or redirecting to login on authentication errors. The callback is called for every API error before the error is thrown to React Query.

import { createSDKProvider, type ApiDomainError } from '@rabstack/rab-react-sdk';
import { toast } from 'your-toast-library';

const { SDKProvider, useQuery, useMutation } = createSDKProvider({
  apiSpecs,
  // Global error handler - called for ALL API errors
  onError: (error: ApiDomainError) => {
    // Log to error tracking service
    console.error('API Error:', {
      status: error.status,
      url: error.url,
      method: error.method,
      message: error.message,
      errorCode: error.errorCode,
    });

    // Show toast notification for certain errors
    if (error.isServerError()) {
      toast.error('Server error. Please try again later.');
    } else if (error.isNetworkError()) {
      toast.error('Network error. Check your connection.');
    }

    // Redirect to login on unauthorized errors
    if (error.isUnauthorized()) {
      window.location.href = '/login';
    }

    // Send to analytics
    analytics.track('api_error', {
      status: error.status,
      endpoint: error.url,
      errorCode: error.errorCode,
    });
  },
});

You can also provide or override the error handler at the provider level:

function App() {
  return (
    <SDKProvider
      baseUrl="https://api.example.com"
      onError={(error) => {
        // Provider-level error handler - overrides config-level handler
        if (error.isValidationError()) {
          toast.error(error.getAllMessages().join(', '));
        }
      }}
    >
      <YourApp />
    </SDKProvider>
  );
}

Note: The onError callback is called in addition to React Query's onError handlers on individual queries/mutations. Use the global handler for cross-cutting concerns (logging, analytics) and local handlers for component-specific error handling (displaying error messages, resetting forms, etc.).

Automatic Query Invalidation

The mutation hook supports automatic query invalidation via the invalidateQueries option. When a mutation settles (succeeds or fails), it will automatically invalidate the specified query keys, triggering a refetch.

const createProduct = useDonationMutation('createProduct', {
  // These queries will be invalidated when the mutation completes
  invalidateQueries: ['listProducts', 'getCategories', 'getDashboard'],
  onSuccess: (data) => {
    console.log('Product created and queries invalidated!');
  },
});

// When you call this mutation:
createProduct.mutate({ body: { name: 'New Product' } });
// After completion, 'listProducts', 'getCategories', and 'getDashboard' queries
// will be automatically invalidated and refetched

Benefits:

  • Type-safe: Only valid endpoint names are allowed in invalidateQueries
  • Automatic: No need to manually call queryClient.invalidateQueries()
  • Works with onSettled: Your custom onSettled callback is still called
  • Handles errors: Invalidation happens on both success and error

Example: Complete CRUD workflow

function ProductManager() {
  // Query
  const { data: products } = useDonationQuery('listProducts');

  // Create - invalidates list
  const createProduct = useDonationMutation('createProduct', {
    invalidateQueries: ['listProducts'],
  });

  // Update - invalidates list and detail
  const updateProduct = useDonationMutation('updateProduct', {
    invalidateQueries: ['listProducts', 'getProduct'],
  });

  // Delete - invalidates list
  const deleteProduct = useDonationMutation('deleteProduct', {
    invalidateQueries: ['listProducts'],
  });

  return (
    <div>
      <button onClick={() => createProduct.mutate({ body: { name: 'New' } })}>
        Create
      </button>
      {products?.map((product) => (
        <div key={product.id}>
          <span>{product.name}</span>
          <button onClick={() => updateProduct.mutate({
            params: { productId: product.id },
            body: { name: 'Updated' }
          })}>
            Update
          </button>
          <button onClick={() => deleteProduct.mutate({
            params: { productId: product.id }
          })}>
            Delete
          </button>
        </div>
      ))}
    </div>
  );
}

Query Options and Retry Logic

You can pass any React Query options to customize behavior:

const { data, error } = useDonationQuery('getProfile', {
  retry: (failureCount, error) => {
    // Don't retry on client errors (4xx) - all errors are ApiDomainError
    if (error.isClientError()) {
      return false;
    }
    // Retry server errors up to 3 times
    return failureCount < 3;
  },
  staleTime: 5 * 60 * 1000, // 5 minutes
  refetchOnWindowFocus: false,
  onError: (err) => {
    // All errors from the SDK are ApiDomainError instances
    if (err.isUnauthorized()) {
      // Redirect to login
      window.location.href = '/login';
    }
  },
});

Custom Query Parameter Encoding

By default, query parameters are encoded as standard URL query strings. You can customize this behavior:

// Custom query encoder (e.g., for nested objects, arrays, etc.)
function customParseQuery(query: any): string {
  // Example: Custom encoding for complex filters
  const params = new URLSearchParams();

  Object.entries(query).forEach(([key, value]) => {
    if (Array.isArray(value)) {
      // Encode arrays as comma-separated values
      params.append(key, value.join(','));
    } else if (typeof value === 'object' && value !== null) {
      // Encode objects as JSON
      params.append(key, JSON.stringify(value));
    } else if (value !== undefined && value !== null) {
      params.append(key, String(value));
    }
  });

  return params.toString();
}

const { SDKProvider, useQuery } = createSDKProvider({
  apiSpecs,
  parseQuery: customParseQuery, // Use custom query encoder
});

// Or disable custom encoding for a specific request
const { data } = useQuery('listProducts', {
  query: { tags: ['sale', 'featured'], category: 'electronics' },
  encodeQuery: false, // Skip custom encoder, use default encoding
});

Response Format Handling

Default Behavior:

The SDK automatically unwraps responses in the { data: ... } format:

// If your API returns: { data: { id: 1, name: "Product" } }
// The SDK automatically extracts and returns: { id: 1, name: "Product" }

const { data } = useDonationQuery('getProduct', { params: { productId: '1' } });
// data is typed as Product, not { data: Product }

// If your API returns data directly without wrapping, that's fine too:
// API returns: { id: 1, name: "Product" }
// SDK returns: { id: 1, name: "Product" }

This means you can define your endpoint response types as the actual data shape, not the wrapper:

// ✅ Correct - define the actual data type
const getProduct = GetApi<Product>('/products/:productId');

// ❌ Not needed - don't include the wrapper
const getProduct = GetApi<{ data: Product }>('/products/:productId');

Custom Response Transformation:

If your API uses a different response format, you can customize the transformation:

import { createSDKProvider, defaultTransformResponse } from '@rabstack/rab-react-sdk';

const { SDKProvider, useQuery, useMutation } = createSDKProvider({
  apiSpecs,

  // Custom response transformer
  transformResponse: (data, response) => {
    // Example 1: Extract from nested structure
    // API returns: { result: { success: true, payload: {...} } }
    if (data.result?.payload) {
      return data.result.payload;
    }

    // Example 2: Transform dates
    if (data.createdAt) {
      return {
        ...data,
        createdAt: new Date(data.createdAt),
      };
    }

    // Fallback to default behavior
    return defaultTransformResponse(data);
  },
});

Common Transformation Examples:

// Stripe-style: { object: 'charge', data: {...} }
transformResponse: (data) => data.data || data

// Nested payload: { success: true, payload: {...} }
transformResponse: (data) => data.payload

// Array wrapper: { results: [...] }
transformResponse: (data) => data.results || data

// No transformation
transformResponse: (data) => data

// Access response headers
transformResponse: (data, response) => ({
  ...data,
  rateLimit: response.headers.get('X-RateLimit-Remaining'),
})

The transformResponse function receives:

  • data - The parsed JSON response
  • response - The original Response object (for accessing headers, status, etc.)

Optimistic Updates

const updateProduct = useDonationMutation('updateProduct', {
  onMutate: async (variables) => {
    // Cancel outgoing refetches
    await queryClient.cancelQueries({ queryKey: ['listProducts'] });

    // Snapshot previous value
    const previous = queryClient.getQueryData(['listProducts']);

    // Optimistically update
    queryClient.setQueryData(['listProducts'], (old) => {
      // Update logic
    });

    return { previous };
  },
  onError: (err, variables, context) => {
    // Rollback on error
    queryClient.setQueryData(['listProducts'], context?.previous);
  },
});

Server-Side SDK with Next.js

The SDK provides a server-compatible entry point for use in Next.js Server Components, API routes, and other server-side contexts. It supports custom fetch functions and extended request options for features like Next.js revalidation.

Basic Server-Side Usage

// lib/server-sdk.ts
import { createSDK } from '@rabstack/rab-react-sdk/server';
import { apiSpecs } from './api-specs';

export const serverSdk = createSDK({
  apiSpecs,
  baseUrl: process.env.API_URL!,
  getAuthorization: () => ({
    Authorization: `Bearer ${process.env.API_SECRET}`,
  }),
});

// app/products/page.tsx (Server Component)
import { serverSdk } from '@/lib/server-sdk';

export default async function ProductsPage() {
  const products = await serverSdk.listProducts();

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

Custom Fetch with Next.js Revalidation

For Next.js Server Components, you can pass a custom fetch function with extended options like next.revalidate for ISR (Incremental Static Regeneration) and next.tags for on-demand revalidation.

Step 1: Define Extended Request Options

// lib/next-sdk.ts
import { createSDK, CustomFetchRequest } from '@rabstack/rab-react-sdk/server';
import { apiSpecs } from './api-specs';

// Define Next.js-specific request options
interface NextRequestOptions {
  next?: {
    revalidate?: number | false;
    tags?: string[];
  };
  cache?: RequestCache;
}

// Create SDK with extended options
export const serverSdk = createSDK<typeof apiSpecs, NextRequestOptions>({
  apiSpecs,
  baseUrl: process.env.API_URL!,
  getAuthorization: () => ({
    Authorization: `Bearer ${process.env.API_SECRET}`,
  }),
  // Custom fetch that passes Next.js options
  customFetch: async (request: CustomFetchRequest<NextRequestOptions>) => {
    return fetch(request.url, {
      method: request.method,
      headers: request.headers,
      body: request.body,
      next: request.next,
      cache: request.cache,
    });
  },
});

Step 2: Use in Server Components with Revalidation

// app/products/page.tsx
import { serverSdk } from '@/lib/next-sdk';

export default async function ProductsPage() {
  // Revalidate every 60 seconds (ISR)
  const products = await serverSdk.listProducts({
    next: { revalidate: 60 },
  });

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

// app/products/[id]/page.tsx
import { serverSdk } from '@/lib/next-sdk';

export default async function ProductPage({ params }: { params: { id: string } }) {
  // Use tags for on-demand revalidation
  const product = await serverSdk.getProduct({
    params: { productId: params.id },
    next: {
      revalidate: 3600, // 1 hour
      tags: [`product-${params.id}`],
    },
  });

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

Step 3: On-Demand Revalidation (API Route)

// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache';
import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest) {
  const { productId } = await request.json();

  // Revalidate the specific product
  revalidateTag(`product-${productId}`);

  return NextResponse.json({ revalidated: true });
}

Common Patterns

No Cache (Always Fresh)

const data = await serverSdk.getProfile({
  cache: 'no-store',
});

Static with Revalidation

const data = await serverSdk.listProducts({
  next: { revalidate: 300 }, // 5 minutes
});

Force Cache (Static)

const data = await serverSdk.getSettings({
  cache: 'force-cache',
});

Tagged for On-Demand Revalidation

const data = await serverSdk.getUser({
  params: { userId },
  next: {
    revalidate: false, // Never auto-revalidate
    tags: ['users', `user-${userId}`],
  },
});

Type Safety

The extended options are fully type-safe. TypeScript will autocomplete and validate the options:

// ✅ Valid - all options are typed
serverSdk.getProduct({
  params: { productId: '123' },
  next: { revalidate: 60, tags: ['products'] },
  cache: 'force-cache',
});

// ❌ TypeScript error - invalid cache value
serverSdk.getProduct({
  params: { productId: '123' },
  cache: 'invalid-value', // Error!
});

What's New in v0.0.1

New Features

Spec-Based SDK Creation

  • New createSDKProvider function that generates SDK, provider, and hooks from endpoint specifications
  • Clean helper functions: GetApi(), PostApi(), PutApi(), PatchApi(), DeleteApi() for defining endpoints
  • Define your API once as specs, get type-safe methods automatically
  • No need to manually write fetch logic for each endpoint

Enhanced Error Handling

  • All SDK errors are guaranteed to be ApiDomainError instances (no need for instanceof checks)
  • New ApiDomainError class with helper methods:
    • isNetworkError() - Detects network/connection errors
    • isUnauthorized(), isForbidden(), isNotFound()
    • isValidationError(), isConflict()
    • isClientError(), isServerError()
    • getAllMessages(), getDisplayMessage()
  • Customize error extraction with extractApiError option

Utility Functions

  • buildUrlPath() - Build URLs with parameter replacement
  • createSDK() - Low-level SDK creation from specs
  • createRequestFunction() - Customizable request handler

Improved Developer Experience

  • Automatic response unwrapping for { data: ... } format
  • Custom response transformation with transformResponse option
  • Custom query parameter encoding with parseQuery option
  • Better TypeScript type inference
  • Cleaner API with less boilerplate

Future Work / Roadmap

Factory-Based SDK Support (Planned)

We're considering adding support for a factory-based approach that allows you to wrap existing SDKs or create custom SDK implementations. This would be useful for:

  • Existing SDKs: Wrap third-party or legacy SDKs with React Query hooks
  • Custom Logic: Implement custom request/response handling (GraphQL, gRPC, etc.)
  • Complex Transformations: Handle non-standard API patterns
  • Third-Party Integration: Integrate with existing API clients

Potential API (Subject to Change):

// Create SDK with custom factory
const { SDKProvider, useSDK } = createSDKProviderWithFactory<
  MySDK,
  { baseUrl: string; apiKey: string }
>((props) => createMyCustomSDK(props.baseUrl, props.apiKey));

// Create typed hooks from existing SDK
export const useMyQuery = createSDKQueryHook<MySDK>(useMySDK);
export const useMyMutation = createSDKMutationHook<MySDK>(useMySDK);

Status: Under consideration. If you need this feature, please upvote or comment on the GitHub issue.

License

MIT

Contributing

Contributions are welcome! Please feel free to submit a Pull Request to the rabstack repository.

For issues and feature requests, please visit the issue tracker.