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:
- Return cached data (stale)
- Fetch new data (revalidate)
- 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
| Library | Size | Type | Best For | Learning Curve |
|---|---|---|---|---|
| Axios | ~15KB | HTTP Client | REST APIs, request/response interceptors | Easy |
| TanStack Query | ~40KB | State Management | Caching, background updates, complex state | Moderate |
| SWR | ~4KB | React Hooks | Simple caching, Next.js apps | Easy |
| Apollo Client | ~33KB | GraphQL Client | GraphQL APIs, normalized cache | Moderate-Hard |
| tRPC | ~10KB | Type-safe RPC | Full-stack TypeScript, type safety | Moderate |
| Refine | ~100KB+ | Framework | Admin panels, CRUD apps | Moderate |
| ky | ~2KB | Fetch Wrapper | Modern fetch alternative | Easy |
| ofetch | ~3KB | Fetch Wrapper | Universal fetch with JSON parsing | Easy |
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:
- API type (REST vs GraphQL)
- Project scale (simple vs complex)
- Bundle size constraints
- Type safety requirements
- Team expertise
- 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.