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

@mshindi-labs/swrkit

v1.0.3

Published

A powerful, apisauce-inspired wrapper around SWR for standardized API data fetching in React/Next.js applications

Readme

SWRKit

A powerful, apisauce-inspired wrapper around SWR for standardized API data fetching in React/Next.js applications.

Features

  • Automatic Authentication: Automatically reads access tokens from cookies and adds to headers
  • Standardized Response Format: Consistent response structure across all API calls
  • Error Classification: Automatic error categorization (client, server, network, timeout)
  • Request/Response Transforms: Modify requests and responses with middleware
  • Monitors: Observe all API responses for logging and analytics
  • Context Provider: Shared configuration across your entire app
  • TypeScript First: Full type safety with generics
  • SWR Powered: Built on top of SWR for optimal caching and revalidation
  • Multiple Hooks: useFetch, useMutation, useInfiniteScroll
  • Cookie Utilities: Helper functions for cookie management
  • Compatible with Refetch: Uses the same error codes and response format

Installation

Install SWRKit using your preferred package manager:

# npm
npm install @mshindi-labs/swrkit

# yarn
yarn add @mshindi-labs/swrkit

# pnpm
pnpm add @mshindi-labs/swrkit

# bun
bun add @mshindi-labs/swrkit

Peer Dependencies:

SWRKit requires the following peer dependencies:

# npm
npm install swr react react-dom

# yarn
yarn add swr react react-dom

# pnpm
pnpm add swr react react-dom

# bun
bun add swr react react-dom

Quick Start

1. Wrap Your App with the Provider

// app/layout.tsx or _app.tsx
import { SWRKitProvider } from '@/lib/swrkit';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <SWRKitProvider
      config={{
        baseURL: process.env.NEXT_PUBLIC_API_URL,
        headers: {
          'Content-Type': 'application/json',
        },
        timeout: 10000,
      }}
    >
      {children}
    </SWRKitProvider>
  );
}

2. Use the Hooks in Your Components

import { useFetch, useMutation } from '@/lib/swrkit';

// GET request
function UserProfile({ userId }: { userId: string }) {
  const { data, isLoading, error, problem } = useFetch<User>(`/users/${userId}`);

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

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

// POST request
function CreateUserForm() {
  const { trigger, isMutating } = useMutation<User, CreateUserData>({
    url: '/users',
    method: 'POST',
    onSuccess: (data) => console.log('User created:', data),
  });

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    await trigger({ name: 'John', email: '[email protected]' });
  };

  return (
    <form onSubmit={handleSubmit}>
      <button disabled={isMutating}>
        {isMutating ? 'Creating...' : 'Create User'}
      </button>
    </form>
  );
}

Automatic Authentication

SWRKit automatically reads access tokens from cookies and adds them to request headers. No manual configuration needed!

Default Behavior

By default, SWRKit will:

  1. Read the access_token cookie
  2. Add it to the Authorization header with Bearer prefix
  3. Apply to all requests automatically
// Just wrap your app - authentication is automatic!
<SWRKitProvider
  config={{
    baseURL: process.env.NEXT_PUBLIC_API_URL,
  }}
>
  {children}
</SWRKitProvider>

// All requests will automatically include:
// Authorization: Bearer <token-from-cookie>

Custom Auth Configuration

Customize the authentication behavior:

<SWRKitProvider
  config={{
    baseURL: process.env.NEXT_PUBLIC_API_URL,
    auth: {
      // Custom cookie name (default: 'access_token')
      accessTokenCookie: 'my_auth_token',

      // Custom header name (default: 'Authorization')
      authHeader: 'X-Auth-Token',

      // Custom token prefix (default: 'Bearer')
      tokenPrefix: 'Token',

      // Disable automatic auth (default: true)
      autoAuth: true,
    },
  }}
>
  {children}
</SWRKitProvider>

Disable Auto Auth

If you want to handle authentication manually:

<SWRKitProvider
  config={{
    baseURL: process.env.NEXT_PUBLIC_API_URL,
    auth: {
      autoAuth: false,  // Disable automatic auth
    },
    requestTransforms: [
      // Add your custom auth logic
      (config) => {
        const token = getCustomToken();
        if (token) {
          config.headers = {
            ...config.headers,
            Authorization: `Bearer ${token}`,
          };
        }
      },
    ],
  }}
>
  {children}
</SWRKitProvider>

Cookie Utilities

SWRKit provides SSR-safe cookie utilities using cookies-next:

import {
  getCookie,
  setCookie,
  deleteCookie,
  getAccessToken,
  getRefreshToken
} from '@/lib/swrkit';

// Get any cookie (works in both client and server)
const value = getCookie('my_cookie');

// Set a cookie
setCookie('my_cookie', 'value', {
  days: 7,              // Automatically converted to maxAge
  path: '/',
  secure: true,
  sameSite: 'strict',   // lowercase for cookies-next compatibility
  httpOnly: false,      // Only works server-side
});

// Delete a cookie
deleteCookie('my_cookie');

// Get auth tokens
const accessToken = getAccessToken();  // default: 'access_token' cookie
const refreshToken = getRefreshToken();  // default: 'refresh_token' cookie

// With custom cookie names
const customToken = getAccessToken('custom_token_name');

Note: These utilities are SSR-compatible and work in both client and server components thanks to cookies-next.

Auth Config Interface

interface AuthConfig {
  // Cookie name for access token (default: 'access_token')
  accessTokenCookie?: string;

  // Header name for authorization (default: 'Authorization')
  authHeader?: string;

  // Token prefix (default: 'Bearer')
  tokenPrefix?: string;

  // Enable automatic auth from cookies (default: true)
  autoAuth?: boolean;
}

Example: Login Flow

import { setCookie, useMutation } from '@/lib/swrkit';

function LoginForm() {
  const { trigger, isMutating } = useMutation<{ token: string }, LoginData>({
    url: '/auth/login',
    method: 'POST',
    onSuccess: (data) => {
      // Save token to cookie
      setCookie('access_token', data.token, {
        days: 7,
        secure: true,
        sameSite: 'strict',  // lowercase for cookies-next
      });

      // All subsequent requests will now include the token automatically!
      router.push('/dashboard');
    },
  });

  return (
    <form onSubmit={(e) => {
      e.preventDefault();
      trigger({ email, password });
    }}>
      <button disabled={isMutating}>Login</button>
    </form>
  );
}

Example: Logout Flow

import { deleteCookie, mutate } from '@/lib/swrkit';

function LogoutButton() {
  const handleLogout = () => {
    // Delete the token cookie
    deleteCookie('access_token');

    // Clear all SWR cache
    mutate(() => true, undefined, { revalidate: false });

    // Redirect to login
    router.push('/login');
  };

  return <button onClick={handleLogout}>Logout</button>;
}

Core Hooks

useFetch<T> - GET Requests

The main hook for fetching data with automatic caching and revalidation.

const { data, isLoading, error, problem, mutate } = useFetch<User>(
  '/users/1',
  {
    params: { include: 'posts' },
    swr: {
      revalidateOnFocus: false,
      dedupingInterval: 60000,
    },
  }
);

Parameters:

  • url (string | null): The API endpoint (null to skip fetching)
  • options (UseFetchOptions): Configuration options

Returns:

  • data: Response data (typed)
  • isLoading: Whether the initial load is happening
  • isValidating: Whether a revalidation is happening
  • error: Error object if request failed
  • problem: Problem code (PROBLEM_CODE enum)
  • status: HTTP status code
  • headers: Response headers
  • mutate: Function to manually update the cache

Options:

interface UseFetchOptions<T> {
  swr?: SWRConfiguration<T>;  // SWR-specific config
  request?: SWRKitRequestConfig;  // Request config
  params?: Record<string, any>;  // Query parameters
  skip?: boolean;  // Skip the request conditionally
}

useMutation<TData, TVariables> - POST/PUT/PATCH/DELETE

Hook for data mutations with automatic cache invalidation.

const { trigger, isMutating, data, error, problem, reset } = useMutation<
  User,
  UpdateUserData
>({
  url: '/users/1',
  method: 'PUT',
  onSuccess: (data, variables) => {
    console.log('Updated:', data);
  },
  onError: (error, variables) => {
    console.error('Failed:', error);
  },
  invalidateKeys: ['/users', '/users/1'],
});

// Trigger the mutation
await trigger({ name: 'Jane Doe' });

Parameters:

  • options (UseMutationOptions): Mutation configuration

Options:

interface UseMutationOptions<TData, TVariables> {
  url: string;  // API endpoint
  method?: 'POST' | 'PUT' | 'PATCH' | 'DELETE';  // HTTP method
  request?: SWRKitRequestConfig;  // Additional config
  onSuccess?: (data: TData, variables: TVariables) => void | Promise<void>;
  onError?: (error: Error, variables: TVariables) => void | Promise<void>;
  invalidateKeys?: string[];  // Keys to revalidate on success
}

Returns:

  • trigger: Function to trigger the mutation
  • isMutating: Whether mutation is in progress
  • data: Last successful mutation data
  • error: Last mutation error
  • problem: Problem code from last mutation
  • reset: Reset mutation state

useInfiniteScroll<T> - Pagination

Hook for infinite scrolling and pagination.

const { data, isLoading, loadMore, hasMore, isLoadingMore } = useInfiniteScroll<User>({
  getKey: (pageIndex, previousPageData) => {
    if (previousPageData && previousPageData.length === 0) return null;
    return `/users?page=${pageIndex + 1}&limit=20`;
  },
  pageSize: 20,
});

// Load more when scrolling
<button onClick={loadMore} disabled={!hasMore || isLoadingMore}>
  {isLoadingMore ? 'Loading...' : 'Load More'}
</button>

Options:

interface UseInfiniteScrollOptions<T> {
  getKey: (pageIndex: number, previousPageData: T | null) => string | null;
  swr?: SWRConfiguration;
  request?: SWRKitRequestConfig;
  pageSize?: number;
}

Returns:

  • data: Array of all pages
  • isLoading: Whether first page is loading
  • isValidating: Whether any page is validating
  • error: Error object
  • loadMore: Function to load next page
  • hasMore: Whether there are more pages
  • isLoadingMore: Whether loading additional pages
  • refresh: Refresh all pages
  • size: Number of pages loaded

Advanced Features

Request Transforms

Modify requests before they're sent (e.g., add authentication):

import { SWRKitProvider, useSWRKitContext } from '@/lib/swrkit';

// In provider setup
<SWRKitProvider
  config={{
    baseURL: 'https://api.example.com',
    requestTransforms: [
      (config) => {
        const token = localStorage.getItem('token');
        if (token) {
          config.headers = {
            ...config.headers,
            Authorization: `Bearer ${token}`,
          };
        }
      },
    ],
  }}
>
  {children}
</SWRKitProvider>

// Or add dynamically in components
function App() {
  const { addRequestTransform } = useSWRKitContext();

  useEffect(() => {
    addRequestTransform(async (config) => {
      const token = await getAuthToken();
      config.headers = {
        ...config.headers,
        Authorization: `Bearer ${token}`,
      };
    });
  }, [addRequestTransform]);

  return <YourApp />;
}

Response Transforms

Modify responses after they're received:

<SWRKitProvider
  config={{
    responseTransforms: [
      (response) => {
        // Transform snake_case to camelCase
        if (response.ok && response.data) {
          response.data = transformKeys(response.data, camelCase);
        }
      },
    ],
  }}
>
  {children}
</SWRKitProvider>

Monitors

Observe all API responses for logging, analytics, or error tracking:

<SWRKitProvider
  config={{
    monitors: [
      // Logging monitor
      (response) => {
        console.log('API Response:', {
          url: response.data,
          status: response.status,
          problem: response.problem,
          duration: response.duration,
        });
      },

      // Error tracking monitor
      (response) => {
        if (!response.ok) {
          trackError({
            type: response.problem,
            error: response.originalError,
            status: response.status,
          });
        }
      },

      // Performance monitoring
      (response) => {
        if (response.duration && response.duration > 3000) {
          console.warn('Slow request detected:', response.duration);
        }
      },
    ],
  }}
>
  {children}
</SWRKitProvider>

Dynamic Configuration

Update configuration at runtime:

function Settings() {
  const { setHeader, setBaseURL, addMonitor } = useSWRKitContext();

  const updateApiKey = (apiKey: string) => {
    setHeader('X-API-Key', apiKey);
  };

  const switchEnvironment = (env: 'prod' | 'dev') => {
    setBaseURL(env === 'prod'
      ? 'https://api.example.com'
      : 'https://dev-api.example.com'
    );
  };

  return (
    <div>
      <button onClick={() => updateApiKey('new-key')}>Update API Key</button>
      <button onClick={() => switchEnvironment('dev')}>Switch to Dev</button>
    </div>
  );
}

Response Format

All responses follow a standardized format:

interface SWRKitResponse<T> {
  ok: boolean;  // true if 200-299
  problem: PROBLEM_CODE | null;  // Error classification
  originalError: Error | null;  // Original error object
  data?: T;  // Response data
  status?: number;  // HTTP status code
  headers?: Record<string, string>;  // Response headers
  duration?: number;  // Request duration in ms
}

Error Handling

Problem Codes

export enum PROBLEM_CODE {
  NONE = 'NONE',  // 200-299 (success)
  CLIENT_ERROR = 'CLIENT_ERROR',  // 400-499
  SERVER_ERROR = 'SERVER_ERROR',  // 500-599
  TIMEOUT_ERROR = 'TIMEOUT_ERROR',  // Request timeout
  CONNECTION_ERROR = 'CONNECTION_ERROR',  // Cannot connect
  NETWORK_ERROR = 'NETWORK_ERROR',  // Network unavailable
  CANCEL_ERROR = 'CANCEL_ERROR',  // Request cancelled
  UNKNOWN_ERROR = 'UNKNOWN_ERROR',  // Unknown error
}

Error Handling Patterns

const { data, error, problem } = useFetch<User>('/users/1');

// Check for specific errors
if (problem === PROBLEM_CODE.CLIENT_ERROR) {
  return <div>Client error (4xx)</div>;
}

if (problem === PROBLEM_CODE.SERVER_ERROR) {
  return <div>Server error (5xx)</div>;
}

if (problem === PROBLEM_CODE.NETWORK_ERROR) {
  return <div>No internet connection</div>;
}

// Or use switch
switch (problem) {
  case PROBLEM_CODE.TIMEOUT_ERROR:
    return <div>Request timed out. Please try again.</div>;
  case PROBLEM_CODE.CLIENT_ERROR:
    return <div>Invalid request</div>;
  default:
    return <div>Something went wrong</div>;
}

Integration Patterns

With TanStack Query

You can use both SWRKit and TanStack Query in the same app:

// Use SWRKit for real-time data with auto-revalidation
const { data: liveData } = useFetch('/live-feed');

// Use TanStack Query for complex data fetching
const { data: complexData } = useQuery({
  queryKey: ['complex'],
  queryFn: () => fetchComplex(),
});

With Next.js Server Components

// Server Component (for initial data)
async function UserPage({ params }: { params: { id: string } }) {
  const initialData = await fetch(`/api/users/${params.id}`).then(r => r.json());

  return <UserClient userId={params.id} initialData={initialData} />;
}

// Client Component (with SWR for real-time updates)
'use client';
function UserClient({ userId, initialData }: Props) {
  const { data = initialData } = useFetch<User>(`/users/${userId}`);

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

Conditional Fetching

function UserPosts({ userId }: { userId: string | null }) {
  // Fetch only when userId is available
  const { data } = useFetch<Post[]>(
    userId ? `/users/${userId}/posts` : null
  );

  // Or use skip option
  const { data: data2 } = useFetch<Post[]>('/posts', {
    skip: !userId,
  });

  return <PostList posts={data} />;
}

Dependent Queries

function UserWithPosts({ userId }: { userId: string }) {
  // First fetch user
  const { data: user } = useFetch<User>(`/users/${userId}`);

  // Then fetch posts only when user is loaded
  const { data: posts } = useFetch<Post[]>(
    user ? `/users/${user.id}/posts` : null
  );

  return (
    <div>
      <h1>{user?.name}</h1>
      <PostList posts={posts} />
    </div>
  );
}

Optimistic Updates

function LikeButton({ postId }: { postId: string }) {
  const { data, mutate } = useFetch<Post>(`/posts/${postId}`);
  const { trigger } = useMutation({
    url: `/posts/${postId}/like`,
    method: 'POST',
  });

  const handleLike = async () => {
    // Optimistically update UI
    mutate(
      (current) => current ? { ...current, liked: true, likes: current.likes + 1 } : current,
      false
    );

    // Send request
    await trigger({});

    // Revalidate
    mutate();
  };

  return (
    <button onClick={handleLike}>
      {data?.liked ? 'Unlike' : 'Like'} ({data?.likes})
    </button>
  );
}

Cache Manipulation

import { mutate } from '@/lib/swrkit';

// Update cache imperatively
await mutate('/users/1', updatedUser);

// Invalidate cache (trigger revalidation)
await mutate('/users/1');

// Update multiple keys
await Promise.all([
  mutate('/users'),
  mutate('/users/1'),
]);

Preloading Data

import { preload, useSWRKitFetcher } from '@/lib/swrkit';

function HomePage() {
  const fetcher = useSWRKitFetcher();

  const handleMouseEnter = () => {
    // Preload user profile before navigation
    preload('/users/1', undefined, fetcher);
  };

  return (
    <Link href="/users/1" onMouseEnter={handleMouseEnter}>
      View Profile
    </Link>
  );
}

Configuration Options

Provider Config

interface SWRKitConfig {
  baseURL?: string;  // Base URL for all requests
  headers?: HeadersInit;  // Default headers
  timeout?: number;  // Default timeout (ms)
  swr?: SWRConfiguration;  // SWR config
  requestTransforms?: RequestTransform[];  // Request middleware
  responseTransforms?: ResponseTransform[];  // Response middleware
  monitors?: Monitor[];  // Response observers
  fetcher?: Fetcher;  // Custom fetcher function
}

SWR Configuration

You can pass any SWR configuration through the provider:

<SWRKitProvider
  config={{
    swr: {
      revalidateOnFocus: false,
      revalidateOnReconnect: true,
      dedupingInterval: 2000,
      focusThrottleInterval: 5000,
      shouldRetryOnError: true,
      errorRetryCount: 3,
      errorRetryInterval: 5000,
      keepPreviousData: false,
    },
  }}
>
  {children}
</SWRKitProvider>

Best Practices

  1. Use the Provider: Always wrap your app with SWRKitProvider
  2. Type Your Responses: Use generics for type safety
  3. Handle Errors Gracefully: Use problem codes for consistent error handling
  4. Use Transforms: Add auth tokens and transform data with transforms
  5. Monitor Performance: Use monitors for logging and analytics
  6. Invalidate Smartly: Use invalidateKeys in mutations to keep cache fresh
  7. Conditional Fetching: Use skip or null URL for conditional requests
  8. Optimize Bundle: Import only what you need

TypeScript Types

import type {
  SWRKitResponse,
  SWRKitConfig,
  UseFetchOptions,
  UseFetchReturn,
  UseMutationOptions,
  UseMutationReturn,
  PROBLEM_CODE,
} from '@/lib/swrkit';

Why Use SWRKit?

SWRKit bridges the gap between the simplicity of SWR and the structure needed for production applications. Here's why you should use it:

1. Standardized API Response Format

Consistent error handling across your entire application with predictable response structures. No more guessing what shape errors will take.

const { data, problem, status } = useFetch('/api/user');

// Always know what went wrong
if (problem === PROBLEM_CODE.CLIENT_ERROR) {
  // Handle 4xx errors
} else if (problem === PROBLEM_CODE.NETWORK_ERROR) {
  // Handle offline state
}

2. Automatic Authentication

Stop copying auth logic to every API call. SWRKit automatically reads tokens from cookies and adds them to headers.

// Just wrap your app - authentication is automatic
<SWRKitProvider config={{ baseURL: process.env.NEXT_PUBLIC_API_URL }}>
  <App />
</SWRKitProvider>

// All requests automatically include: Authorization: Bearer <token>

3. Built-in Optimistic Updates

Update UI instantly, roll back on errors automatically. No manual state juggling.

const { trigger } = useMutation({
  url: '/api/posts/123/like',
  method: 'POST',
  optimisticData: (current) => ({ ...current, liked: true, likes: current.likes + 1 }),
  rollbackOnError: true, // Automatically reverts on error
});

4. Powerful Middleware System

Extend functionality without modifying core code. Log requests, cache to localStorage, keep stale data visible - all with simple middleware.

const { data } = useFetch('/api/user', {
  use: [
    laggyMiddleware(), // No loading spinners during revalidation
    loggerMiddleware(), // Debug all requests
    localStorageMiddleware({ key: 'user-cache' }), // Persist offline
  ],
});

5. Compatible with Refetch

Already using @mshindi-labs/refetch? SWRKit uses the same PROBLEM_CODE enum and response format, making migration seamless.

// Refetch (imperative)
const response = await api.get('/users/1');

// SWRKit (declarative with automatic caching)
const { data } = useFetch('/users/1');

6. Production-Ready Features Out of the Box

  • TypeScript-first - Full type safety with generics
  • Error classification - CLIENT_ERROR, SERVER_ERROR, NETWORK_ERROR, TIMEOUT_ERROR
  • Request/response transforms - Modify data at a global level
  • Monitors - Track all API calls for analytics
  • Testing utilities - Mock fetchers and test providers
  • WebSocket support - useSubscription for real-time data
  • Infinite scroll - useInfiniteScroll with automatic pagination

7. Better Developer Experience

  • Comprehensive TypeScript types
  • Detailed JSDoc documentation
  • Built-in testing utilities
  • Works seamlessly with Next.js App Router
  • SWR DevTools support

Who Should Use SWRKit?

Perfect for:

  • Next.js applications needing standardized API patterns
  • Teams wanting consistent error handling across the app
  • Projects requiring automatic authentication
  • Applications with real-time data requirements
  • Codebases migrating from @mshindi-labs/refetch

Consider alternatives if:

  • You need a non-React solution (SWR is React-only)
  • You prefer imperative API calls over hooks
  • Your app doesn't benefit from automatic revalidation

Migration from Refetch

SWRKit is compatible with Refetch and uses the same problem codes:

// Refetch
const response = await api.get<User>('/users/1');
if (response.ok) {
  console.log(response.data);
}

// SWRKit
const { data, problem } = useFetch<User>('/users/1');
if (!problem) {
  console.log(data);
}

Contributing

Contributions are welcome! Please read CONTRIBUTING.md for details on our code of conduct and the process for submitting pull requests.

License

MIT License - see LICENSE file for details.