API Integration Modern Implementation
1. Axios Fetch API Error Handling
| Feature | Axios | Fetch API | Advantage |
|---|---|---|---|
| HTTP Client | Third-party library | Native browser API | Fetch: No dependency |
| Error Handling | Automatic error throwing | Manual response.ok check | Axios: Simpler errors |
| Request/Response Interceptors | Built-in | Manual wrapper needed | Axios: More features |
| JSON Parsing | Automatic | Manual .json() call | Axios: Convenience |
| Timeout | Built-in timeout option | AbortController needed | Axios: Native timeout |
| Request Cancellation | CancelToken / AbortController | AbortController | Both support cancellation |
Example: Axios vs Fetch API with comprehensive error handling
// Install Axios
npm install axios
// Axios configuration with interceptors
import axios from 'axios';
const apiClient = axios.create({
baseURL: 'https://api.example.com',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
// Request interceptor
apiClient.interceptors.request.use(
(config) => {
// Add auth token
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
// Add timestamp
config.metadata = { startTime: new Date() };
return config;
},
(error) => {
return Promise.reject(error);
}
);
// Response interceptor
apiClient.interceptors.response.use(
(response) => {
// Log response time
const endTime = new Date();
const duration = endTime - response.config.metadata.startTime;
console.log(`Request to ${response.config.url} took ${duration}ms`);
return response.data;
},
async (error) => {
const originalRequest = error.config;
// Handle 401 - Refresh token
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
const { data } = await axios.post('/auth/refresh');
localStorage.setItem('token', data.token);
originalRequest.headers.Authorization = `Bearer ${data.token}`;
return apiClient(originalRequest);
} catch (refreshError) {
// Redirect to login
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
// Handle different error types
if (error.response) {
// Server responded with error status
console.error('Response error:', {
status: error.response.status,
data: error.response.data,
headers: error.response.headers,
});
} else if (error.request) {
// Request made but no response
console.error('Network error:', error.request);
} else {
// Error in request setup
console.error('Request setup error:', error.message);
}
return Promise.reject(error);
}
);
// Usage examples
async function fetchUsers() {
try {
const users = await apiClient.get('/users');
return users;
} catch (error) {
if (axios.isAxiosError(error)) {
if (error.code === 'ECONNABORTED') {
console.error('Request timeout');
} else if (error.response?.status === 404) {
console.error('Users not found');
}
}
throw error;
}
}
// Parallel requests
async function fetchDashboardData() {
try {
const [users, posts, comments] = await Promise.all([
apiClient.get('/users'),
apiClient.get('/posts'),
apiClient.get('/comments'),
]);
return { users, posts, comments };
} catch (error) {
console.error('Dashboard fetch failed:', error);
throw error;
}
}
// Request cancellation with Axios
const controller = new AbortController();
apiClient.get('/users', {
signal: controller.signal,
})
.then(data => console.log(data))
.catch(error => {
if (error.code === 'ERR_CANCELED') {
console.log('Request cancelled');
}
});
// Cancel the request
controller.abort();
// Fetch API with error handling
async function fetchWithErrorHandling(url, options = {}) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);
try {
const response = await fetch(url, {
...options,
signal: controller.signal,
headers: {
'Content-Type': 'application/json',
...options.headers,
},
});
clearTimeout(timeoutId);
// Check for HTTP errors
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error('Request timeout');
}
throw error;
}
}
// Fetch wrapper with interceptors
class APIClient {
constructor(baseURL) {
this.baseURL = baseURL;
this.requestInterceptors = [];
this.responseInterceptors = [];
}
addRequestInterceptor(fn) {
this.requestInterceptors.push(fn);
}
addResponseInterceptor(fn) {
this.responseInterceptors.push(fn);
}
async request(endpoint, options = {}) {
let url = `${this.baseURL}${endpoint}`;
let config = { ...options };
// Apply request interceptors
for (const interceptor of this.requestInterceptors) {
const result = await interceptor({ url, config });
url = result.url;
config = result.config;
}
try {
const response = await fetch(url, config);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
let data = await response.json();
// Apply response interceptors
for (const interceptor of this.responseInterceptors) {
data = await interceptor(data);
}
return data;
} catch (error) {
console.error('Request failed:', error);
throw error;
}
}
get(endpoint, options) {
return this.request(endpoint, { ...options, method: 'GET' });
}
post(endpoint, body, options) {
return this.request(endpoint, {
...options,
method: 'POST',
body: JSON.stringify(body),
headers: {
'Content-Type': 'application/json',
...options?.headers,
},
});
}
}
// Usage
const api = new APIClient('https://api.example.com');
api.addRequestInterceptor(({ url, config }) => {
const token = localStorage.getItem('token');
if (token) {
config.headers = {
...config.headers,
Authorization: `Bearer ${token}`,
};
}
return { url, config };
});
api.addResponseInterceptor((data) => {
console.log('Response received:', data);
return data;
});
// React hook for API calls
import { useState, useEffect } from 'react';
function useAPI(url, options = {}) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
async function fetchData() {
try {
setLoading(true);
const response = await apiClient.get(url, {
...options,
signal: controller.signal,
});
setData(response);
setError(null);
} catch (err) {
if (err.name !== 'CanceledError') {
setError(err);
}
} finally {
setLoading(false);
}
}
fetchData();
return () => controller.abort();
}, [url]);
return { data, loading, error };
}
// Usage
function UsersList() {
const { data: users, loading, error } = useAPI('/users');
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
2. GraphQL Apollo Client Codegen
| Feature | Implementation | Benefit | Tool |
|---|---|---|---|
| Apollo Client | GraphQL client library | Caching, state management | @apollo/client |
| Code Generation | Types from schema | Type safety, autocomplete | graphql-codegen |
| Type-safe Hooks | Generated React hooks | Typed queries/mutations | @graphql-codegen/typescript-react-apollo |
| Normalized Cache | Automatic data normalization | Efficient updates, consistency | InMemoryCache |
| Optimistic Updates | Update UI before response | Better UX | optimisticResponse option |
| Error Handling | GraphQL + network errors | Granular error info | onError link |
Example: GraphQL with Apollo Client and code generation
// Install dependencies
npm install @apollo/client graphql
npm install --save-dev @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-react-apollo
// codegen.yml configuration
schema: 'https://api.example.com/graphql'
documents: 'src/**/*.graphql'
generates:
src/generated/graphql.ts:
plugins:
- typescript
- typescript-operations
- typescript-react-apollo
config:
withHooks: true
withHOC: false
withComponent: false
// package.json scripts
{
"scripts": {
"codegen": "graphql-codegen --config codegen.yml",
"codegen:watch": "graphql-codegen --config codegen.yml --watch"
}
}
// GraphQL queries (src/queries/users.graphql)
query GetUsers {
users {
id
name
email
avatar
}
}
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
posts {
id
title
content
}
}
}
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
}
}
mutation UpdateUser($id: ID!, $input: UpdateUserInput!) {
updateUser(id: $id, input: $input) {
id
name
email
}
}
// Apollo Client setup
import { ApolloClient, InMemoryCache, ApolloProvider, createHttpLink } from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
import { onError } from '@apollo/client/link/error';
// HTTP link
const httpLink = createHttpLink({
uri: 'https://api.example.com/graphql',
});
// Auth link
const authLink = setContext((_, { headers }) => {
const token = localStorage.getItem('token');
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : '',
},
};
});
// Error link
const errorLink = onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors) {
graphQLErrors.forEach(({ message, locations, path }) => {
console.error(`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`);
});
}
if (networkError) {
console.error(`[Network error]: ${networkError}`);
}
});
// Create Apollo Client
const client = new ApolloClient({
link: errorLink.concat(authLink.concat(httpLink)),
cache: new InMemoryCache({
typePolicies: {
User: {
keyFields: ['id'],
},
Post: {
keyFields: ['id'],
},
},
}),
defaultOptions: {
watchQuery: {
fetchPolicy: 'cache-and-network',
errorPolicy: 'all',
},
query: {
fetchPolicy: 'network-only',
errorPolicy: 'all',
},
mutate: {
errorPolicy: 'all',
},
},
});
// App with ApolloProvider
function App() {
return (
<ApolloProvider client={client}>
<YourApp />
</ApolloProvider>
);
}
// Use generated hooks (TypeScript)
import { useGetUsersQuery, useGetUserQuery, useCreateUserMutation } from './generated/graphql';
function UsersList() {
const { data, loading, error, refetch } = useGetUsersQuery({
pollInterval: 5000, // Poll every 5 seconds
});
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<button onClick={() => refetch()}>Refresh</button>
<ul>
{data?.users.map(user => (
<li key={user.id}>{user.name} - {user.email}</li>
))}
</ul>
</div>
);
}
// Single user with variables
function UserProfile({ userId }: { userId: string }) {
const { data, loading, error } = useGetUserQuery({
variables: { id: userId },
skip: !userId, // Skip query if no userId
});
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!data?.user) return <div>User not found</div>;
return (
<div>
<h1>{data.user.name}</h1>
<p>{data.user.email}</p>
<h2>Posts</h2>
<ul>
{data.user.posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}
// Mutation with optimistic response
function CreateUserForm() {
const [createUser, { loading, error }] = useCreateUserMutation({
update(cache, { data }) {
// Update cache after mutation
cache.modify({
fields: {
users(existingUsers = []) {
const newUserRef = cache.writeFragment({
data: data?.createUser,
fragment: gql`
fragment NewUser on User {
id
name
email
}
`,
});
return [...existingUsers, newUserRef];
},
},
});
},
optimisticResponse: {
createUser: {
__typename: 'User',
id: 'temp-id',
name: 'Loading...',
email: 'loading@example.com',
},
},
});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const formData = new FormData(e.target as HTMLFormElement);
try {
await createUser({
variables: {
input: {
name: formData.get('name') as string,
email: formData.get('email') as string,
},
},
});
} catch (err) {
console.error('Mutation error:', err);
}
};
return (
<form onSubmit={handleSubmit}>
<input name="name" placeholder="Name" required />
<input name="email" type="email" placeholder="Email" required />
<button type="submit" disabled={loading}>
{loading ? 'Creating...' : 'Create User'}
</button>
{error && <div>Error: {error.message}</div>}
</form>
);
}
// Lazy query (load on demand)
import { useGetUserLazyQuery } from './generated/graphql';
function SearchUser() {
const [getUser, { data, loading }] = useGetUserLazyQuery();
const handleSearch = (userId: string) => {
getUser({ variables: { id: userId } });
};
return (
<div>
<button onClick={() => handleSearch('123')}>Load User</button>
{loading && <div>Loading...</div>}
{data && <div>{data.user?.name}</div>}
</div>
);
}
// Subscription example
subscription OnUserCreated {
userCreated {
id
name
email
}
}
// Use subscription
import { useOnUserCreatedSubscription } from './generated/graphql';
function RealtimeUsers() {
const { data } = useOnUserCreatedSubscription();
useEffect(() => {
if (data?.userCreated) {
console.log('New user created:', data.userCreated);
}
}, [data]);
return <div>Listening for new users...</div>;
}
// Fragment colocation
const USER_FRAGMENT = gql`
fragment UserFields on User {
id
name
email
avatar
}
`;
const GET_USERS_WITH_FRAGMENT = gql`
${USER_FRAGMENT}
query GetUsers {
users {
...UserFields
}
}
`;
// Cache manipulation
import { useApolloClient } from '@apollo/client';
function UserActions() {
const client = useApolloClient();
const updateUserInCache = (userId: string, updates: Partial<User>) => {
client.cache.modify({
id: client.cache.identify({ __typename: 'User', id: userId }),
fields: {
name: () => updates.name,
email: () => updates.email,
},
});
};
return <button onClick={() => updateUserInCache('1', { name: 'Updated' })}>Update</button>;
}
3. tRPC Type-safe API Calls
| Feature | Description | Benefit | Use Case |
|---|---|---|---|
| End-to-end Type Safety | TypeScript from server to client | No manual type definitions | Full-stack TypeScript apps |
| No Code Generation | Direct type inference | Simpler setup vs GraphQL | Monorepo projects |
| React Query Integration | Built on @tanstack/react-query | Caching, optimistic updates | Modern React apps |
| Zod Validation | Runtime input validation | Type-safe validation | Robust API contracts |
| Lightweight | Small bundle size | Better performance | Bundle-conscious apps |
| Autocomplete | Full IDE support | Developer experience | All TypeScript projects |
Example: tRPC full-stack type-safe implementation
// Install tRPC
npm install @trpc/server @trpc/client @trpc/react-query @trpc/next @tanstack/react-query zod
// Server setup (server/trpc.ts)
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
// Create tRPC context
export const createContext = async ({ req, res }) => {
return {
user: req.user,
prisma: prisma, // or your database client
};
};
type Context = Awaited<ReturnType<typeof createContext>>;
// Initialize tRPC
const t = initTRPC.context<Context>().create();
// Export reusable router and procedure helpers
export const router = t.router;
export const publicProcedure = t.procedure;
// Protected procedure with auth middleware
const isAuthed = t.middleware(({ ctx, next }) => {
if (!ctx.user) {
throw new Error('Not authenticated');
}
return next({
ctx: {
user: ctx.user,
},
});
});
export const protectedProcedure = t.procedure.use(isAuthed);
// Define router (server/routers/user.ts)
import { router, publicProcedure, protectedProcedure } from '../trpc';
import { z } from 'zod';
const userRouter = router({
// Query - get all users
getAll: publicProcedure
.query(async ({ ctx }) => {
return await ctx.prisma.user.findMany();
}),
// Query with input - get user by ID
getById: publicProcedure
.input(z.object({
id: z.string(),
}))
.query(async ({ ctx, input }) => {
return await ctx.prisma.user.findUnique({
where: { id: input.id },
});
}),
// Mutation - create user
create: protectedProcedure
.input(z.object({
name: z.string().min(3).max(50),
email: z.string().email(),
age: z.number().int().min(18).optional(),
}))
.mutation(async ({ ctx, input }) => {
return await ctx.prisma.user.create({
data: input,
});
}),
// Mutation - update user
update: protectedProcedure
.input(z.object({
id: z.string(),
name: z.string().min(3).max(50).optional(),
email: z.string().email().optional(),
}))
.mutation(async ({ ctx, input }) => {
const { id, ...data } = input;
return await ctx.prisma.user.update({
where: { id },
data,
});
}),
// Mutation - delete user
delete: protectedProcedure
.input(z.object({
id: z.string(),
}))
.mutation(async ({ ctx, input }) => {
return await ctx.prisma.user.delete({
where: { id: input.id },
});
}),
});
// Main app router (server/routers/index.ts)
import { router } from '../trpc';
import { userRouter } from './user';
import { postRouter } from './post';
export const appRouter = router({
user: userRouter,
post: postRouter,
});
export type AppRouter = typeof appRouter;
// Next.js API route (pages/api/trpc/[trpc].ts)
import { createNextApiHandler } from '@trpc/server/adapters/next';
import { appRouter } from '../../../server/routers';
import { createContext } from '../../../server/trpc';
export default createNextApiHandler({
router: appRouter,
createContext,
onError: ({ path, error }) => {
console.error(`tRPC Error on '${path}':`, error);
},
});
// Client setup (utils/trpc.ts)
import { httpBatchLink } from '@trpc/client';
import { createTRPCNext } from '@trpc/next';
import type { AppRouter } from '../server/routers';
export const trpc = createTRPCNext<AppRouter>({
config({ ctx }) {
return {
links: [
httpBatchLink({
url: '/api/trpc',
headers() {
return {
authorization: `Bearer ${localStorage.getItem('token')}`,
};
},
}),
],
queryClientConfig: {
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 minute
},
},
},
};
},
ssr: false,
});
// Wrap app with tRPC provider (pages/_app.tsx)
import { trpc } from '../utils/trpc';
import type { AppProps } from 'next/app';
function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />;
}
export default trpc.withTRPC(MyApp);
// Use tRPC in components - fully type-safe!
import { trpc } from '../utils/trpc';
function UsersList() {
// Query - automatically typed!
const { data: users, isLoading, error } = trpc.user.getAll.useQuery();
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{users?.map(user => (
<li key={user.id}>{user.name} - {user.email}</li>
))}
</ul>
);
}
// Query with input
function UserProfile({ userId }: { userId: string }) {
const { data: user } = trpc.user.getById.useQuery(
{ id: userId },
{ enabled: !!userId } // Only fetch if userId exists
);
return <div>{user?.name}</div>;
}
// Mutation
function CreateUserForm() {
const utils = trpc.useContext();
const createUser = trpc.user.create.useMutation({
onSuccess: () => {
// Invalidate and refetch users list
utils.user.getAll.invalidate();
},
onError: (error) => {
console.error('Failed to create user:', error);
},
});
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
createUser.mutate({
name: formData.get('name') as string,
email: formData.get('email') as string,
});
};
return (
<form onSubmit={handleSubmit}>
<input name="name" required />
<input name="email" type="email" required />
<button type="submit" disabled={createUser.isLoading}>
{createUser.isLoading ? 'Creating...' : 'Create'}
</button>
{createUser.error && <div>{createUser.error.message}</div>}
</form>
);
}
// Optimistic updates
function UpdateUserForm({ userId }: { userId: string }) {
const utils = trpc.useContext();
const updateUser = trpc.user.update.useMutation({
onMutate: async (newData) => {
// Cancel outgoing refetches
await utils.user.getById.cancel({ id: userId });
// Snapshot previous value
const previousUser = utils.user.getById.getData({ id: userId });
// Optimistically update
utils.user.getById.setData({ id: userId }, (old) => ({
...old!,
...newData,
}));
return { previousUser };
},
onError: (err, newData, context) => {
// Rollback on error
utils.user.getById.setData({ id: userId }, context?.previousUser);
},
onSettled: () => {
// Refetch after error or success
utils.user.getById.invalidate({ id: userId });
},
});
return <div>...</div>;
}
// Prefetch for faster navigation
function UserLink({ userId }: { userId: string }) {
const utils = trpc.useContext();
const handleMouseEnter = () => {
// Prefetch user data on hover
utils.user.getById.prefetch({ id: userId });
};
return (
<a href={`/users/${userId}`} onMouseEnter={handleMouseEnter}>
View User
</a>
);
}
// Infinite query for pagination
const infiniteUserRouter = router({
getInfinite: publicProcedure
.input(z.object({
limit: z.number().min(1).max(100).default(10),
cursor: z.string().optional(),
}))
.query(async ({ ctx, input }) => {
const users = await ctx.prisma.user.findMany({
take: input.limit + 1,
cursor: input.cursor ? { id: input.cursor } : undefined,
});
let nextCursor: string | undefined;
if (users.length > input.limit) {
const nextItem = users.pop();
nextCursor = nextItem!.id;
}
return {
users,
nextCursor,
};
}),
});
// Use infinite query
function InfiniteUsersList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = trpc.user.getInfinite.useInfiniteQuery(
{ limit: 10 },
{
getNextPageParam: (lastPage) => lastPage.nextCursor,
}
);
return (
<div>
{data?.pages.map((page, i) => (
<div key={i}>
{page.users.map(user => (
<div key={user.id}>{user.name}</div>
))}
</div>
))}
{hasNextPage && (
<button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
{isFetchingNextPage ? 'Loading...' : 'Load More'}
</button>
)}
</div>
);
}
4. React Query Infinite Queries
| Feature | Hook | Purpose | Use Case |
|---|---|---|---|
| useQuery | Fetch data | GET requests, caching | List, detail views |
| useMutation | Modify data | POST/PUT/DELETE | Forms, updates |
| useInfiniteQuery | Paginated data | Load more pattern | Infinite scroll |
| Optimistic Updates | Update before response | Instant UI feedback | Likes, votes, edits |
| Cache Invalidation | Refetch stale data | Keep data fresh | After mutations |
| Prefetching | Load data early | Faster navigation | Hover, route changes |
Example: React Query comprehensive implementation
// Install React Query
npm install @tanstack/react-query @tanstack/react-query-devtools
// Setup QueryClient (App.tsx)
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000,
cacheTime: 5 * 60 * 1000,
refetchOnWindowFocus: false,
retry: 3,
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
// useQuery - Basic fetch
import { useQuery } from '@tanstack/react-query';
function UsersList() {
const { data: users, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then(res => res.json()),
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
// useMutation - POST/PUT/DELETE
import { useMutation, useQueryClient } from '@tanstack/react-query';
function CreateUserForm() {
const queryClient = useQueryClient();
const createUser = useMutation({
mutationFn: (newUser) => {
return fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newUser),
}).then(res => res.json());
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
return (
<form onSubmit={(e) => {
e.preventDefault();
createUser.mutate({ name: 'John', email: 'john@example.com' });
}}>
<button type="submit">Create</button>
</form>
);
}
// useInfiniteQuery - Pagination
import { useInfiniteQuery } from '@tanstack/react-query';
function InfinitePostsList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['posts'],
queryFn: ({ pageParam = 0 }) => {
return fetch(`/api/posts?cursor=${pageParam}`).then(res => res.json());
},
getNextPageParam: (lastPage) => lastPage.nextCursor,
initialPageParam: 0,
});
return (
<div>
{data?.pages.map((page, i) => (
<div key={i}>
{page.posts.map(post => (
<div key={post.id}>{post.title}</div>
))}
</div>
))}
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage ? 'Loading...' : 'Load More'}
</button>
</div>
);
}
// Optimistic Updates
function LikeButton({ postId }) {
const queryClient = useQueryClient();
const likeMutation = useMutation({
mutationFn: (postId) => fetch(`/api/posts/${postId}/like`, { method: 'POST' }),
onMutate: async (postId) => {
await queryClient.cancelQueries({ queryKey: ['post', postId] });
const previousPost = queryClient.getQueryData(['post', postId]);
queryClient.setQueryData(['post', postId], (old) => ({
...old,
likes: old.likes + 1,
}));
return { previousPost };
},
onError: (err, postId, context) => {
queryClient.setQueryData(['post', postId], context.previousPost);
},
});
return <button onClick={() => likeMutation.mutate(postId)}>Like</button>;
}
// Prefetch on hover
function PostLink({ postId }) {
const queryClient = useQueryClient();
return (
<a
href={`/posts/${postId}`}
onMouseEnter={() => {
queryClient.prefetchQuery({
queryKey: ['post', postId],
queryFn: () => fetch(`/api/posts/${postId}`).then(res => res.json()),
});
}}
>
View Post
</a>
);
}
5. MSW Mock Service Worker
| Feature | Purpose | Environment | Benefit |
|---|---|---|---|
| Service Worker API | Intercept network requests | Browser (dev/test) | No code changes |
| Node.js Integration | Mock in tests | Jest, Vitest | Test isolation |
| Type-safe Handlers | TypeScript support | All environments | Catch errors early |
| REST & GraphQL | Both API types | Universal | Flexible mocking |
| Stateful Mocking | Simulate state changes | Development | Realistic behavior |
| Network Simulation | Delays, errors | Testing | Edge case testing |
Example: MSW for API mocking
// Install MSW
npm install msw --save-dev
npx msw init public/ --save
// Create handlers (src/mocks/handlers.ts)
import { http, HttpResponse, graphql } from 'msw';
export const handlers = [
// REST handlers
http.get('/api/users', () => {
return HttpResponse.json([
{ id: '1', name: 'John Doe', email: 'john@example.com' },
{ id: '2', name: 'Jane Smith', email: 'jane@example.com' },
]);
}),
http.get('/api/users/:id', ({ params }) => {
const { id } = params;
return HttpResponse.json({
id,
name: 'John Doe',
email: 'john@example.com',
});
}),
http.post('/api/users', async ({ request }) => {
const newUser = await request.json();
return HttpResponse.json(
{ id: Math.random().toString(), ...newUser },
{ status: 201 }
);
}),
// Simulate errors
http.get('/api/error', () => {
return HttpResponse.json(
{ error: 'Something went wrong' },
{ status: 500 }
);
}),
// Simulate delay
http.get('/api/slow', async () => {
await delay(3000);
return HttpResponse.json({ data: 'slow response' });
}),
// GraphQL handlers
graphql.query('GetUsers', () => {
return HttpResponse.json({
data: {
users: [
{ id: '1', name: 'John' },
{ id: '2', name: 'Jane' },
],
},
});
}),
];
// Browser setup (src/mocks/browser.ts)
import { setupWorker } from 'msw/browser';
import { handlers } from './handlers';
export const worker = setupWorker(...handlers);
// Start in development (src/main.tsx)
async function enableMocking() {
if (process.env.NODE_ENV !== 'development') return;
const { worker } = await import('./mocks/browser');
return worker.start();
}
enableMocking().then(() => {
ReactDOM.createRoot(document.getElementById('root')!).render(<App />);
});
// Test setup (src/mocks/server.ts)
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);
// Setup tests (src/setupTests.ts)
import { beforeAll, afterEach, afterAll } from 'vitest';
import { server } from './mocks/server';
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
// Test with MSW
import { render, screen, waitFor } from '@testing-library/react';
import { server } from './mocks/server';
import { http, HttpResponse } from 'msw';
test('displays users', async () => {
render(<UsersList />);
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
});
test('handles error', async () => {
server.use(
http.get('/api/users', () => {
return HttpResponse.json({ error: 'Failed' }, { status: 500 });
})
);
render(<UsersList />);
await waitFor(() => {
expect(screen.getByText(/error/i)).toBeInTheDocument();
});
});
// Stateful mocking with database
import { factory, primaryKey } from '@mswjs/data';
const db = factory({
user: {
id: primaryKey(String),
name: String,
email: String,
},
});
db.user.create({ id: '1', name: 'John', email: 'john@example.com' });
export const statefulHandlers = [
http.get('/api/users', () => {
return HttpResponse.json(db.user.getAll());
}),
http.post('/api/users', async ({ request }) => {
const newUser = await request.json();
const user = db.user.create({ id: Math.random().toString(), ...newUser });
return HttpResponse.json(user, { status: 201 });
}),
];
6. API Rate Limiting Retry Logic
| Strategy | Implementation | Use Case | Consideration |
|---|---|---|---|
| Exponential Backoff | Delay *= 2 after each retry | 429 rate limit errors | Avoid thundering herd |
| Retry-After Header | Respect server's retry time | Server-specified delays | Most accurate timing |
| Jitter | Add randomness to delay | Prevent synchronized retries | Better distribution |
| Circuit Breaker | Stop after threshold failures | Protect failing services | Fail fast when needed |
| Request Queue | Throttle outgoing requests | Client-side rate limiting | Prevent hitting limits |
| Token Bucket | Allow burst then limit | Smooth traffic patterns | Balance speed and limits |
Example: Advanced retry logic and rate limiting
// Exponential backoff with jitter
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, options);
if (response.ok) return response;
if (response.status === 429 || response.status >= 500) {
const retryAfter = response.headers.get('Retry-After');
let delay;
if (retryAfter) {
delay = parseInt(retryAfter) * 1000;
} else {
const exponentialDelay = Math.pow(2, attempt) * 1000;
const jitter = Math.random() * 1000;
delay = exponentialDelay + jitter;
}
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
throw new Error(`HTTP ${response.status}`);
} catch (error) {
if (attempt === maxRetries) throw error;
}
}
}
// Circuit Breaker
class CircuitBreaker {
constructor(threshold = 5, timeout = 60000) {
this.failures = 0;
this.threshold = threshold;
this.timeout = timeout;
this.state = 'CLOSED';
this.lastFailureTime = null;
}
async execute(fn) {
if (this.state === 'OPEN') {
const timeSinceFailure = Date.now() - this.lastFailureTime;
if (timeSinceFailure > this.timeout) {
this.state = 'HALF_OPEN';
} else {
throw new Error('Circuit breaker is OPEN');
}
}
try {
const result = await fn();
if (this.state === 'HALF_OPEN') {
this.state = 'CLOSED';
this.failures = 0;
}
return result;
} catch (error) {
this.failures++;
this.lastFailureTime = Date.now();
if (this.failures >= this.threshold) {
this.state = 'OPEN';
}
throw error;
}
}
}
const breaker = new CircuitBreaker();
await breaker.execute(() => fetch('/api/data'));
// Request Queue with Rate Limiting
class RequestQueue {
constructor(maxRequestsPerWindow = 10, windowMs = 1000) {
this.queue = [];
this.processing = false;
this.requestsInWindow = 0;
this.windowStart = Date.now();
this.maxRequestsPerWindow = maxRequestsPerWindow;
this.windowMs = windowMs;
}
async add(fn) {
return new Promise((resolve, reject) => {
this.queue.push(async () => {
try {
resolve(await fn());
} catch (error) {
reject(error);
}
});
this.process();
});
}
async process() {
if (this.processing || this.queue.length === 0) return;
this.processing = true;
while (this.queue.length > 0) {
const now = Date.now();
const elapsed = now - this.windowStart;
if (elapsed >= this.windowMs) {
this.requestsInWindow = 0;
this.windowStart = now;
}
if (this.requestsInWindow >= this.maxRequestsPerWindow) {
await new Promise(resolve => setTimeout(resolve, this.windowMs - elapsed));
continue;
}
const fn = this.queue.shift();
this.requestsInWindow++;
await fn();
}
this.processing = false;
}
}
const queue = new RequestQueue(10, 1000);
for (let i = 0; i < 100; i++) {
queue.add(() => fetch(`/api/data/${i}`));
}
// Token Bucket
class TokenBucket {
constructor(capacity, refillRate) {
this.tokens = capacity;
this.capacity = capacity;
this.refillRate = refillRate;
this.lastRefill = Date.now();
}
async consume(tokens = 1) {
this.refill();
if (this.tokens >= tokens) {
this.tokens -= tokens;
return;
}
const tokensNeeded = tokens - this.tokens;
const waitTime = (tokensNeeded / this.refillRate) * 1000;
await new Promise(resolve => setTimeout(resolve, waitTime));
this.refill();
this.tokens -= tokens;
}
refill() {
const now = Date.now();
const elapsed = (now - this.lastRefill) / 1000;
const tokensToAdd = elapsed * this.refillRate;
this.tokens = Math.min(this.capacity, this.tokens + tokensToAdd);
this.lastRefill = now;
}
}
const bucket = new TokenBucket(10, 2);
await bucket.consume(1);
await fetch('/api/data');
// React Query with retry
import { useQuery } from '@tanstack/react-query';
function UserData({ userId }) {
const { data } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetch(`/api/users/${userId}`).then(res => res.json()),
retry: (failureCount, error) => {
if (error.response?.status === 404) return false;
return failureCount < 3;
},
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
});
return <div>{data?.name}</div>;
}
API Integration Best Practices Summary
- Axios vs Fetch - Axios provides interceptors, automatic JSON parsing, built-in timeout; Fetch is native, needs manual error/timeout handling
- GraphQL Apollo - Use @graphql-codegen for type-safe hooks, normalize cache, implement optimistic updates, leverage fragments
- tRPC Type Safety - End-to-end TypeScript without codegen, Zod validation, built on React Query, automatic autocomplete
- React Query Patterns - useQuery for fetching, useMutation for updates, useInfiniteQuery for pagination, optimistic updates, prefetch on hover
- MSW Mocking - Mock APIs in browser with Service Worker, stateful mocking, test without backend, simulate delays/errors
- Retry & Rate Limiting - Exponential backoff with jitter, respect Retry-After headers, circuit breaker, request queue, token bucket
- Production Ready - Comprehensive error handling, automatic token refresh, request cancellation, proper TypeScript types, monitoring