More Data Fetching and API Handling Libraries

Explore additional data fetching libraries and tools including SWR, Apollo Client, tRPC, and fetch alternatives for various use cases in modern web development.

More Data Fetching and API Handling Libraries

Beyond Axios, TanStack Query, and Refine, the JavaScript ecosystem offers many other powerful data fetching and API handling solutions. This lesson explores popular alternatives and specialized tools for different use cases.

SWR - Stale-While-Revalidate

SWR is a lightweight React Hooks library for data fetching created by Vercel (the team behind Next.js).

What is SWR?

SWR implements the "stale-while-revalidate" HTTP caching strategy:

  1. Return cached data (stale)
  2. Fetch new data (revalidate)
  3. Update with fresh data

Key Features

  • Lightweight: Only ~4KB gzipped
  • Fast: Returns cached data immediately
  • Automatic revalidation: Keeps data fresh
  • Built-in cache: No additional setup needed
  • TypeScript support: Full type safety
  • SSR/SSG compatible: Works with Next.js
  • Real-time: Built-in polling and focus revalidation

Installation

yarn add swr

Basic Usage

'use client';

import useSWR from 'swr';

const fetcher = (url: string) => fetch(url).then(r => r.json());

function Profile() {
  const { data, error, isLoading } = useSWR('/api/user', fetcher);

  if (error) return <div>Failed to load</div>;
  if (isLoading) return <div>Loading...</div>;

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

Advanced SWR Features

import useSWR from 'swr';

function Users() {
  const { data, error, isLoading, mutate } = useSWR(
    '/api/users',
    fetcher,
    {
      refreshInterval: 3000, // Poll every 3 seconds
      revalidateOnFocus: true, // Refetch when window gains focus
      revalidateOnReconnect: true, // Refetch when internet reconnects
      dedupingInterval: 2000, // Dedupe requests within 2 seconds
      onSuccess: (data) => {
        console.log('Data loaded:', data);
      },
      onError: (error) => {
        console.error('Error:', error);
      }
    }
  );

  // Manual revalidation
  const handleRefresh = () => mutate();

  return (
    <div>
      <button onClick={handleRefresh}>Refresh</button>
      {/* Render users */}
    </div>
  );
}

SWR with Mutations

import useSWR, { useSWRConfig } from 'swr';

function UserProfile() {
  const { mutate } = useSWRConfig();
  const { data } = useSWR('/api/user', fetcher);

  const updateUser = async (newData: any) => {
    // Optimistic update
    mutate('/api/user', newData, false);

    // Send request
    await fetch('/api/user', {
      method: 'PUT',
      body: JSON.stringify(newData)
    });

    // Revalidate
    mutate('/api/user');
  };

  return <div>{/* UI */}</div>;
}

When to Use SWR

Use SWR when:

  • You want a lightweight solution
  • Working with Next.js
  • Need simple caching with automatic revalidation
  • Want minimal configuration

Avoid SWR when:

  • Need complex query orchestration
  • Require advanced mutations
  • Want comprehensive DevTools
  • Need infinite queries out of the box

Apollo Client - GraphQL Client

Apollo Client is the most popular GraphQL client for React applications.

What is Apollo Client?

Apollo Client is a comprehensive state management library for JavaScript that enables you to manage both local and remote data with GraphQL.

Key Features

  • Declarative data fetching: Request exactly what you need
  • Normalized cache: Intelligent caching system
  • Optimistic UI: Update UI before server responds
  • Pagination: Built-in pagination support
  • Subscriptions: Real-time updates via WebSocket
  • Local state management: Manage local state with GraphQL
  • DevTools: Powerful Apollo DevTools browser extension

Installation

yarn add @apollo/client graphql

Setup

// lib/apollo-client.ts
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';

const client = new ApolloClient({
  link: new HttpLink({
    uri: 'https://api.example.com/graphql',
  }),
  cache: new InMemoryCache(),
});

export default client;
// app/layout.tsx
'use client';

import { ApolloProvider } from '@apollo/client';
import client from '@/lib/apollo-client';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <ApolloProvider client={client}>
          {children}
        </ApolloProvider>
      </body>
    </html>
  );
}

Basic Queries

'use client';

import { useQuery, gql } from '@apollo/client';

const GET_USERS = gql`
  query GetUsers {
    users {
      id
      name
      email
    }
  }
`;

function UsersList() {
  const { loading, error, data } = useQuery(GET_USERS);

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

  return (
    <ul>
      {data.users.map((user: any) => (
        <li key={user.id}>
          {user.name} - {user.email}
        </li>
      ))}
    </ul>
  );
}

Mutations

import { useMutation, gql } from '@apollo/client';

const CREATE_USER = gql`
  mutation CreateUser($name: String!, $email: String!) {
    createUser(name: $name, email: $email) {
      id
      name
      email
    }
  }
`;

function CreateUserForm() {
  const [createUser, { loading, error }] = useMutation(CREATE_USER, {
    refetchQueries: [{ query: GET_USERS }],
  });

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget as HTMLFormElement);

    await createUser({
      variables: {
        name: formData.get('name'),
        email: formData.get('email'),
      },
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="name" placeholder="Name" required />
      <input name="email" type="email" placeholder="Email" required />
      <button disabled={loading}>
        {loading ? 'Creating...' : 'Create User'}
      </button>
      {error && <div>Error: {error.message}</div>}
    </form>
  );
}

When to Use Apollo Client

Use Apollo when:

  • Your API is GraphQL
  • Need powerful caching
  • Want real-time subscriptions
  • Require normalized cache

Avoid Apollo when:

  • Using REST APIs (use Axios or TanStack Query instead)
  • Want minimal bundle size
  • Don't need GraphQL features

tRPC - End-to-End Type Safety

tRPC enables you to build fully type-safe APIs without schemas or code generation.

What is tRPC?

tRPC allows you to create type-safe API routes and consume them from your frontend with full TypeScript inference - no code generation required.

Key Features

  • End-to-end type safety: Types automatically inferred
  • No code generation: Direct TypeScript inference
  • RPC-style: Call backend functions like local functions
  • Lightweight: Minimal runtime overhead
  • Framework agnostic: Works with Express, Fastify, Next.js
  • Subscriptions: Real-time support via WebSockets

Installation

yarn add @trpc/server @trpc/client @trpc/react-query @trpc/next
yarn add @tanstack/react-query

Setup

// server/trpc.ts
import { initTRPC } from '@trpc/server';

const t = initTRPC.create();

export const router = t.router;
export const publicProcedure = t.procedure;
// server/routers/_app.ts
import { router, publicProcedure } from '../trpc';
import { z } from 'zod';

export const appRouter = router({
  hello: publicProcedure
    .input(z.object({ name: z.string() }))
    .query(({ input }) => {
      return { greeting: `Hello ${input.name}!` };
    }),

  getUsers: publicProcedure.query(async () => {
    const users = await db.user.findMany();
    return users;
  }),

  createUser: publicProcedure
    .input(z.object({
      name: z.string(),
      email: z.string().email(),
    }))
    .mutation(async ({ input }) => {
      const user = await db.user.create({ data: input });
      return user;
    }),
});

export type AppRouter = typeof appRouter;
// lib/trpc.ts (client)
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '@/server/routers/_app';

export const trpc = createTRPCReact<AppRouter>();

Using tRPC

'use client';

import { trpc } from '@/lib/trpc';

function UsersList() {
  // Fully typed!
  const { data, isLoading } = trpc.getUsers.useQuery();

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

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

function CreateUser() {
  const utils = trpc.useContext();
  
  const createUser = trpc.createUser.useMutation({
    onSuccess: () => {
      // Invalidate and refetch
      utils.getUsers.invalidate();
    },
  });

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget as HTMLFormElement);
    
    createUser.mutate({
      name: formData.get('name') as string,
      email: formData.get('email') as string,
    });
  };

  return <form onSubmit={handleSubmit}>{/* form fields */}</form>;
}

When to Use tRPC

Use tRPC when:

  • Full-stack TypeScript project
  • Want end-to-end type safety
  • Control both frontend and backend
  • Using Next.js or similar framework

Avoid tRPC when:

  • Backend is not TypeScript
  • Need public REST API
  • Third-party API integration
  • Team prefers GraphQL

Fetch Alternatives

ky - Modern Fetch Wrapper

Ky is a tiny and elegant HTTP client based on the Fetch API.

yarn add ky
import ky from 'ky';

// Simple usage
const users = await ky.get('https://api.example.com/users').json();

// With options
const user = await ky.post('https://api.example.com/users', {
  json: { name: 'John', email: 'john@example.com' },
  headers: { Authorization: 'Bearer token' },
  timeout: 5000,
  retry: 3,
}).json();

// Error handling
try {
  await ky.post('https://api.example.com/users', { json: userData });
} catch (error) {
  if (error.name === 'HTTPError') {
    const errorJson = await error.response.json();
    console.error(errorJson);
  }
}

Use when: You want a modern, lightweight fetch wrapper with better defaults.

ofetch - Fetch with Better Defaults

Created by Nuxt team, ofetch provides a better fetch experience.

yarn add ofetch
import { ofetch } from 'ofetch';

// Automatic JSON parsing
const users = await ofetch('https://api.example.com/users');

// With base URL
const api = ofetch.create({
  baseURL: 'https://api.example.com',
  headers: { Authorization: 'Bearer token' }
});

const users = await api('/users');
const user = await api('/users', {
  method: 'POST',
  body: { name: 'John' }
});

Use when: You want a fetch-like API with automatic JSON parsing and better error handling.

Comparison Matrix

LibrarySizeTypeBest ForLearning Curve
Axios~15KBHTTP ClientREST APIs, request/response interceptorsEasy
TanStack Query~40KBState ManagementCaching, background updates, complex stateModerate
SWR~4KBReact HooksSimple caching, Next.js appsEasy
Apollo Client~33KBGraphQL ClientGraphQL APIs, normalized cacheModerate-Hard
tRPC~10KBType-safe RPCFull-stack TypeScript, type safetyModerate
Refine~100KB+FrameworkAdmin panels, CRUD appsModerate
ky~2KBFetch WrapperModern fetch alternativeEasy
ofetch~3KBFetch WrapperUniversal fetch with JSON parsingEasy

Decision Guide

Choose Axios when:

  • Need robust HTTP client
  • Want request/response interceptors
  • Prefer promise-based API
  • Support older browsers

Choose TanStack Query when:

  • Need sophisticated caching
  • Want automatic background updates
  • Building complex data-driven apps
  • Need optimistic updates

Choose SWR when:

  • Using Next.js
  • Want lightweight solution
  • Need simple caching
  • Prefer minimal configuration

Choose Apollo Client when:

  • API is GraphQL
  • Need normalized caching
  • Want real-time subscriptions
  • Require advanced GraphQL features

Choose tRPC when:

  • Full-stack TypeScript project
  • Want end-to-end type safety
  • Control both frontend and backend
  • No need for public REST API

Choose Refine when:

  • Building admin panel or dashboard
  • Need complete CRUD solution
  • Want rapid development
  • Require authentication and permissions

Choose ky/ofetch when:

  • Need lightweight fetch alternative
  • Want better defaults than native fetch
  • Don't need caching or state management
  • Prefer modern, simple API

Combining Libraries

You can combine libraries for more power:

Axios + TanStack Query

import axios from 'axios';
import { useQuery } from '@tanstack/react-query';

const api = axios.create({
  baseURL: 'https://api.example.com'
});

function Users() {
  const { data } = useQuery({
    queryKey: ['users'],
    queryFn: async () => {
      const response = await api.get('/users');
      return response.data;
    }
  });

  return <div>{/* Render users */}</div>;
}

SWR + Axios

import useSWR from 'swr';
import axios from 'axios';

const fetcher = (url: string) => axios.get(url).then(res => res.data);

function Profile() {
  const { data } = useSWR('/api/user', fetcher);
  return <div>{data?.name}</div>;
}

tRPC + TanStack Query

tRPC is built on TanStack Query, so you get both benefits automatically:

const { data } = trpc.getUsers.useQuery();
// Uses TanStack Query under the hood with full type safety

Best Practices

1. Choose Based on Project Needs

Don't over-engineer. For simple projects:

// Simple project - just fetch
const data = await fetch('/api/users').then(r => r.json());

For complex projects, use appropriate tools.

2. Consider Bundle Size

Check bundle sizes with Bundle Analyzer:

yarn add -D @next/bundle-analyzer

3. Use TypeScript

All modern libraries support TypeScript. Use it:

interface User {
  id: string;
  name: string;
  email: string;
}

const { data } = useQuery<User[]>({
  queryKey: ['users'],
  queryFn: fetchUsers
});

4. Implement Error Boundaries

import { ErrorBoundary } from 'react-error-boundary';

function App() {
  return (
    <ErrorBoundary fallback={<ErrorFallback />}>
      <DataComponent />
    </ErrorBoundary>
  );
}

5. Monitor Performance

Use React DevTools Profiler and browser DevTools to monitor:

  • Request timing
  • Re-render frequency
  • Bundle size impact
  • Cache hit rates

Summary

The JavaScript ecosystem offers diverse data fetching solutions:

  • Axios: Robust HTTP client with interceptors
  • TanStack Query: Powerful caching and state management
  • SWR: Lightweight with smart defaults
  • Apollo Client: Comprehensive GraphQL solution
  • tRPC: End-to-end type safety
  • Refine: Complete CRUD framework
  • ky/ofetch: Modern fetch alternatives

Choose based on:

  1. API type (REST vs GraphQL)
  2. Project scale (simple vs complex)
  3. Bundle size constraints
  4. Type safety requirements
  5. Team expertise
  6. Specific features needed

Most projects will benefit from combining multiple libraries - for example, using Axios for HTTP calls with TanStack Query for caching and state management, or tRPC with its built-in TanStack Query integration for full-stack TypeScript applications.

The key is understanding your project's requirements and choosing tools that solve your specific problems without unnecessary complexity.