@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
ApiDomainErrorclass 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-queryQuick 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, useneverto 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 endpointBODY- The request body type (useunknownif not applicable)QUERY- The query parameters type (useunknownif not applicable)PARAMS- The URL path parameters type (useunknownif not applicable)
Properties:
url: string- The endpoint URL path (supports:paramNameplaceholders)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 toEndpointSpecdefinitionsconfig.getAuthorization?: () => Record<string, string> | undefined- Optional function to provide authorization headersconfig.extractApiError?: (status, statusText, url, method, data) => ApiErrorDetails- Optional custom error extractorconfig.parseQuery?: (query: any) => string- Optional custom query string parserconfig.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 acceptsbaseUrl, optionalgetAuthorization, and optionalonErrorpropsuseSDK- Hook to access the raw SDK instanceuseQuery- Type-safe query hook for GET requestsuseMutation- Type-safe mutation hook for POST/PUT/DELETE requestsSDKContext- 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 toEndpointSpecdefinitionsconfig.baseUrl- The base URL for all API requestsconfig.getAuthorization?: () => Record<string, string> | undefined- Optional function to provide authorization headersconfig.extractApiError?- Optional custom error extractorconfig.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 codeurl: string- Request URLmethod: string- HTTP methodmessage: string- Error messageerrorCode?: string | number- Optional error code from APIerrors?: string[]- Optional array of validation errorsdata?: 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 4xxisServerError(): boolean- Returns true if status is 5xxisUnauthorized(): boolean- Returns true if status is 401isForbidden(): boolean- Returns true if status is 403isNotFound(): boolean- Returns true if status is 404isValidationError(): boolean- Returns true if status is 400 or 422isConflict(): boolean- Returns true if status is 409getAllMessages(): string[]- Returns all error messages including validation errorsgetDisplayMessage(): 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 | undefinedConditional 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
bodyorparamsin a required position, the hook will enforce them - If your SDK method has them as optional (using
?:), the hook makes them optional too queryandheadersare always optionalencodeQueryis 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 refetchedBenefits:
- Type-safe: Only valid endpoint names are allowed in
invalidateQueries - Automatic: No need to manually call
queryClient.invalidateQueries() - Works with onSettled: Your custom
onSettledcallback 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 responseresponse- 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
createSDKProviderfunction 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
ApiDomainErrorinstances (no need forinstanceofchecks) - New
ApiDomainErrorclass with helper methods:isNetworkError()- Detects network/connection errorsisUnauthorized(),isForbidden(),isNotFound()isValidationError(),isConflict()isClientError(),isServerError()getAllMessages(),getDisplayMessage()
- Customize error extraction with
extractApiErroroption
Utility Functions
buildUrlPath()- Build URLs with parameter replacementcreateSDK()- Low-level SDK creation from specscreateRequestFunction()- Customizable request handler
Improved Developer Experience
- Automatic response unwrapping for
{ data: ... }format - Custom response transformation with
transformResponseoption - Custom query parameter encoding with
parseQueryoption - 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.
