npm package: @rabstack/rab-react-sdk
Description: React SDK for building applications with React Query and other utilities
Author: Softin
Latest version: 0.11.1

# @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

```bash
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

```typescript
// 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

```typescript
// 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

```tsx
// 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

```tsx
// 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.

```typescript
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.

```typescript
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.

```typescript
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.

```typescript
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.

```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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

```typescript
// 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:

```typescript
// 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:**
```typescript
getAuthorization: () => {
  const token = localStorage.getItem('token');
  return token ? { Authorization: `Bearer ${token}` } : undefined;
}
```

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

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

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

**Dynamic Provider-Level Authorization:**
```typescript
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:

```typescript
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:

```typescript
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.

```typescript
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:

```typescript
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.

```typescript
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**

```typescript
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:

```typescript
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:

```typescript
// 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:

```typescript
// 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:

```typescript
// ✅ 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:

```typescript
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:**

```typescript
// 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.)

### Non-JSON Responses (File Downloads)

By default, the SDK parses all responses as JSON. For endpoints that return non-JSON data (CSV files, PDFs, images, plain text, etc.), use the `responseType` option to bypass JSON parsing.

**Available response types:**
- `'json'` - Default. Parses response as JSON and applies `transformResponse`.
- `'blob'` - Returns a `Blob` object. Use for file downloads (CSV, PDF, images, etc.).
- `'text'` - Returns the raw response as a string.
- `'arraybuffer'` - Returns an `ArrayBuffer` for binary data processing.

**Example: CSV File Download**

```typescript
// Define endpoint spec with Blob response type
const apiSpecs = {
  exportReport: GetApi<Blob, { startDate: string; endDate: string }>('/reports/export'),
} as const;

// In your component
function ExportButton() {
  const { mutateAsync: exportReport } = useMutation('exportReport');

  const handleExport = async () => {
    const blob = await exportReport({
      query: { startDate: '2024-01-01', endDate: '2024-12-31' },
      responseType: 'blob',
    });

    // Trigger browser download
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = 'report.csv';
    a.click();
    URL.revokeObjectURL(url);
  };

  return <button onClick={handleExport}>Export CSV</button>;
}
```

**Example: Using with useQuery**

```typescript
const { data: csvBlob } = useQuery('exportReport', {
  query: { startDate: '2024-01-01', endDate: '2024-12-31' },
  responseType: 'blob',
  enabled: shouldDownload,
});
```

**Example: Plain Text Response**

```typescript
const apiSpecs = {
  getHealthCheck: GetApi<string>('/health'),
} as const;

const { data: status } = useQuery('getHealthCheck', {
  responseType: 'text',
});
```

**Note:** When using `responseType` other than `'json'`, the `transformResponse` function is skipped — the raw response body is returned directly.

### Optimistic Updates

```typescript
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

```typescript
// 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**

```typescript
// 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**

```typescript
// 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)**

```typescript
// 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)**
```typescript
const data = await serverSdk.getProfile({
  cache: 'no-store',
});
```

**Static with Revalidation**
```typescript
const data = await serverSdk.listProducts({
  next: { revalidate: 300 }, // 5 minutes
});
```

**Force Cache (Static)**
```typescript
const data = await serverSdk.getSettings({
  cache: 'force-cache',
});
```

**Tagged for On-Demand Revalidation**
```typescript
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:

```typescript
// ✅ 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):**

```typescript
// 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](https://github.com/rabstack/rabstack/issues).

## License

MIT

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request to the [rabstack repository](https://github.com/rabstack/rabstack).

For issues and feature requests, please visit the [issue tracker](https://github.com/rabstack/rabstack/issues).