What are the different ways to intercept API requests in a React project?
Six common interception points: (1) axios interceptors for auth headers, retries, logging; (2) a wrapped fetch helper used app-wide; (3) React Query's onError/fetcher global hooks; (4) a Service Worker for network-level intercept (offline caching); (5) MSW for tests/dev mocking; (6) browser DevTools Network throttling/blocking for ad-hoc cases.
Many places to insert logic between your code and the network. Pick by use case.
1. Axios interceptors
Classic. Mutate every request and response in one place.
import axios from 'axios';
const api = axios.create({ baseURL: '/api' });
api.interceptors.request.use(config => {
const token = getToken();
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
api.interceptors.response.use(
res => res,
async err => {
if (err.response?.status === 401) {
await refreshToken();
return api.request(err.config);
}
return Promise.reject(err);
},
);Use for: auth, retries, telemetry, response shape transforms.
2. Wrapped fetch
The native equivalent — wrap fetch in your own function.
export async function apiFetch(input: RequestInfo, init: RequestInit = {}) {
const token = getToken();
const headers = {
'Content-Type': 'application/json',
...(token && { Authorization: `Bearer ${token}` }),
...init.headers,
};
const res = await fetch(input, { ...init, headers });
if (res.status === 401) await refreshToken();
if (!res.ok) throw new ApiError(res.status, await res.text());
return res.json();
}Use everywhere instead of bare fetch — the wrapper is the interception layer.
3. React Query hooks
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 2,
onError: err => toast.error(err.message),
},
mutations: {
onError: err => Sentry.captureException(err),
},
},
});Use for: cross-cutting error handling, retry policy, telemetry. Combine with axios/fetch wrapper for the request side.
4. Service Worker
Lives between page and network — intercepts every request including images, fonts.
self.addEventListener('fetch', event => {
if (event.request.url.includes('/api/static')) {
event.respondWith(
caches.match(event.request).then(r => r || fetch(event.request)),
);
}
});Use for: offline support, PWA caching, edge-of-network logic.
5. MSW (Mock Service Worker)
For tests and dev mocking.
import { http, HttpResponse } from 'msw';
const handlers = [
http.get('/api/user', () => HttpResponse.json({ name: 'Alice' })),
];Use for: integration tests with realistic network behavior, local dev without a backend.
6. Proxy at the dev server
Vite / Next config can proxy requests during dev to handle CORS or route to a staging backend.
export default defineConfig({
server: {
proxy: { '/api': { target: 'https://staging.example.com', changeOrigin: true } },
},
});7. Browser DevTools
Throttle network, block specific URLs, override responses — useful for debugging without code changes.
Picking the right layer
| Need | Layer |
|---|---|
| Add auth header | Axios interceptor or fetch wrapper |
| Retry on 401 | Axios interceptor |
| Toast on mutation errors | React Query global onError |
| Offline support | Service Worker |
| Mock API in tests | MSW |
| Mock API in dev | MSW or dev-server proxy |
A common stack
- Layer 1: fetch wrapper or axios instance — auth, base URL, error normalization.
- Layer 2: React Query — caching, retries, mutation error handling.
- Layer 3: MSW in tests.
- Layer 4: Service Worker if PWA / offline.
Anti-patterns
- Auth logic in every component instead of one wrapper.
- Two interception layers fighting (axios + Query both retrying).
- Service Worker caches stale auth tokens.
- Stubbing global fetch in tests instead of MSW.
Follow-up questions
- •How would you implement token refresh in an axios interceptor?
- •When would you reach for a Service Worker over a fetch wrapper?
- •How do MSW and React Query work together?
Common mistakes
- •Two interception layers both retrying — exponential backoff explosion.
- •Service Worker caching stale auth tokens — debugging nightmare.
- •Stubbing global fetch in tests instead of using MSW.
Performance considerations
- •Each layer adds latency. Fetch wrapper: negligible. Axios interceptor: negligible. Service Worker: tiny dispatch cost but offline cache wins overall.
Edge cases
- •CORS preflight requests can be intercepted but headers behave subtly differently.
- •Streaming responses (SSE/streams) don't work cleanly through some interceptors.
- •Service Workers can serve stale responses after deploys — versioning needed.
Real-world examples
- •Stripe uses interceptors for idempotency keys + retries. PWAs (Twitter Lite, Pinterest) use Service Workers for offline. Most React apps with React Query + axios use the dual-layer pattern.