Back to Articles
7 min

Stop Reinventing the Wheel: Robust API Handling in React

Battle-tested patterns for data fetching, caching, error handling, and type safety in production React apps.

ReactAPITypeScript
Stop Reinventing the Wheel: Robust API Handling in React

Early in my career, I built custom hooks for every API call. Hours spent debugging race conditions, stale data, and cache invalidation.

Then I learned a hard lesson: for complex apps, use battle-tested libraries.

Here's my approach now:

1. Use TanStack Query or SWR

Stop building your own data fetching solution. These libraries handle caching, refetching, optimistic updates, and request deduplication out of the box. They've solved problems you don't even know you have yet.

import { useQuery } from '@tanstack/react-query';
 
function useUser(id: string) {
  return useQuery({
    queryKey: ['user', id],
    queryFn: () => axios.get(`/api/users/${id}`).then(res => res.data),
    staleTime: 5 * 60 * 1000, // 5 minutes
  });
}

The query key ['user', id] becomes your cache key. Same key = same cache. No duplicate requests across components.

TanStack Query also handles window refocus refetching, background updates, and pagination out of the box. These features would take weeks to implement correctly from scratch.

2. Axios for HTTP Clients

Better error handling, interceptors for auth tokens, request/response transformations. Fetch is great, but Axios saves you from writing boilerplate.

const api = axios.create({
  baseURL: '/api',
  timeout: 10000,
});
 
api.interceptors.request.use((config) => {
  const token = getToken();
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});
 
api.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      redirectToLogin();
    }
    return Promise.reject(error);
  }
);

Centralized auth handling. Centralized error interception. No more try/catch blocks scattered across every component.

3. Always Handle Cleanup

Even with libraries, implement AbortController to cancel in-flight requests on unmount. Memory leaks are silent killers.

function useSearch(query: string) {
  return useQuery({
    queryKey: ['search', query],
    queryFn: async ({ signal }) => {
      const { data } = await api.get('/search', {
        params: { q: query },
        signal, // TanStack Query passes AbortSignal automatically
      });
      return data;
    },
    enabled: query.length > 0,
  });
}

TanStack Query passes the AbortSignal to your query function. Use it. When the component unmounts or the query key changes, the previous request is automatically cancelled.

4. Centralize Error Handling

Use error boundaries and global error interceptors. Users shouldn't see cryptic 500 errors or blank screens.

// Global error handler for your API layer
function handleApiError(error: unknown): never {
  if (axios.isAxiosError(error)) {
    const status = error.response?.status;
    const message = error.response?.data?.message ?? 'Something went wrong';
 
    switch (status) {
      case 401: throw new AuthError(message);
      case 403: throw new ForbiddenError(message);
      case 404: throw new NotFoundError(message);
      default:   throw new ApiError(message, status);
    }
  }
  throw new NetworkError('Unable to reach the server');
}
 
// React Error Boundary catches what slips through
<ErrorBoundary fallback={<ErrorFallback />}>
  <App />
</ErrorBoundary>

5. Type Safety with Zod or TypeScript

Validate API responses at runtime. The backend will break your contract eventually — be ready for it.

import { z } from 'zod';
 
const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string().email(),
  role: z.enum(['admin', 'user', 'viewer']),
});
 
type User = z.infer<typeof UserSchema>;
 
async function fetchUser(id: string): Promise<User> {
  const { data } = await api.get(`/users/${id}`);
  return UserSchema.parse(data); // Throws if shape doesn't match
}

If the backend silently changes email from a string to an object, your app doesn't silently break. It fails fast with a clear error message.

The Bottom Line

The real wisdom isn't knowing how to build everything from scratch. It's knowing when to leverage proven solutions so you can focus on building features that matter to your users.

Your time is valuable. Spend it solving business problems, not reinventing solved infrastructure.

Need help architecting your React data layer? Get in touch.