ProductionPerformance10 min read
Best Practices
Production-ready patterns for error handling, retry logic, caching, request cancellation, and TypeScript integration.
Error handling
Create a typed API client that throws structured errors instead of returning raw fetch responses. This makes error handling consistent across your entire codebase.
api-client.ts
javascript
"color:#6b7280">// Typed error class for API errors
class ApiError extends Error {
constructor(
public status: number,
public statusText: string,
message?: string
) {
super(message ?? "color:#86efac">`API ${status}: ${statusText}`);
this.name = "ApiError";
}
}
"color:#c4b5fd">async "color:#c4b5fd">function apiFetch<T>(
url: string,
options?: RequestInit
): Promise<T> {
"color:#c4b5fd">const response = "color:#c4b5fd">await fetch(url, {
...options,
headers: {
"Content-Type": "application/json",
...options?.headers,
},
signal: AbortSignal.timeout(10_000), "color:#6b7280">// 10s timeout
});
"color:#c4b5fd">if (!response.ok) {
"color:#c4b5fd">throw "color:#c4b5fd">new ApiError(response.status, response.statusText);
}
"color:#c4b5fd">return response.json() as Promise<T>;
}HTTP status code reference
| Status | Meaning | Recommended action |
|---|---|---|
200 | Success | Process response normally |
400 | Bad Request | Fix the request — invalid parameters |
404 | Not Found | Resource does not exist |
422 | Unprocessable Entity | Validation failed — check request body |
500 | Internal Server Error | Retry with backoff, report if persistent |
503 | Service Unavailable | API temporarily down — retry after delay |
Retry with exponential backoff
Retry on network errors and 5xx responses, but never on 4xx — those require fixing the request before retrying.
retry.ts
javascript
"color:#c4b5fd">async "color:#c4b5fd">function fetchWithRetry<T>(
url: string,
options?: RequestInit,
retries = 3,
baseDelayMs = 1000
): Promise<T> {
for ("color:#c4b5fd">let attempt = 0; attempt <= retries; attempt++) {
"color:#c4b5fd">try {
"color:#c4b5fd">return "color:#c4b5fd">await apiFetch<T>(url, options);
} "color:#c4b5fd">catch (err) {
"color:#6b7280">// Never retry client errors (4xx) — they won't succeed
"color:#c4b5fd">if (err instanceof ApiError && err.status < 500) {
"color:#c4b5fd">throw err;
}
"color:#6b7280">// Last attempt — give up
"color:#c4b5fd">if (attempt === retries) "color:#c4b5fd">throw err;
"color:#6b7280">// Exponential backoff: 1s → 2s → 4s
"color:#c4b5fd">const delay = baseDelayMs * 2 ** attempt;
"color:#c4b5fd">await "color:#c4b5fd">new Promise((resolve) => setTimeout(resolve, delay));
}
}
"color:#c4b5fd">throw "color:#c4b5fd">new Error("Max retries exceeded");
}Caching with TanStack Query
TanStack Query (formerly React Query) handles caching, background refetching, and deduplication out of the box. It is the recommended approach for any React application.
queries.ts
javascript
"color:#c4b5fd">import { useQuery } "color:#c4b5fd">from "@tanstack/react-query";
"color:#c4b5fd">const BASE_URL = "https:">//hotel-data.k3s.ahsm-krakow.com/api/v1";
"color:#6b7280">// Define query keys as constants to avoid typos
"color:#c4b5fd">export "color:#c4b5fd">const queryKeys = {
properties: ["properties"] as "color:#c4b5fd">const,
property: (id: string) => ["properties", id] as "color:#c4b5fd">const,
roomTypes: (filters?: Record<string, string>) =>
["room-types", filters] as "color:#c4b5fd">const,
rooms: (filters?: Record<string, string>) =>
["rooms", filters] as "color:#c4b5fd">const,
summary: ["summary"] as "color:#c4b5fd">const,
};
"color:#6b7280">// Hook for properties with automatic caching
"color:#c4b5fd">export "color:#c4b5fd">function useProperties(enabled?: boolean) {
"color:#c4b5fd">return useQuery({
queryKey: queryKeys.properties,
queryFn: "color:#c4b5fd">async () => {
"color:#c4b5fd">const params = enabled !== undefined
? "color:#86efac">`?enabled=${enabled}`
: "";
"color:#c4b5fd">const { data } = "color:#c4b5fd">await apiFetch<{ data: Property[] }>(
"color:#86efac">`${BASE_URL}/properties${params}`
);
"color:#c4b5fd">return data;
},
staleTime: 5 * 60 * 1000, "color:#6b7280">// 5 minutes
gcTime: 10 * 60 * 1000, "color:#6b7280">// keep in cache 10 minutes
retry: (failureCount, error) => {
"color:#6b7280">// Don't retry 4xx errors
"color:#c4b5fd">if (error instanceof ApiError && error.status < 500) "color:#c4b5fd">return false;
"color:#c4b5fd">return failureCount < 3;
},
});
}Recommended stale times
Properties
stale: 5 min
gc: 10 min
Room Types
stale: 5 min
gc: 10 min
Rooms
stale: 2 min
gc: 5 min
Summary
stale: 1 min
gc: 3 min
Issues
stale: 30 sec
gc: 2 min
Maintenance
stale: 1 min
gc: 3 min
Request cancellation
Cancel in-flight requests when component unmounts or when new requests supersede old ones. This prevents state updates on unmounted components and race conditions in search interfaces.
SearchRooms.tsx
javascript
"color:#c4b5fd">function SearchRooms() {
"color:#c4b5fd">const [query, setQuery] = useState("");
"color:#c4b5fd">const [results, setResults] = useState([]);
useEffect(() => {
"color:#c4b5fd">if (!query) "color:#c4b5fd">return;
"color:#6b7280">// Cancel any in-flight request when query changes
"color:#c4b5fd">const controller = "color:#c4b5fd">new AbortController();
fetch("color:#86efac">`${BASE_URL}/rooms?property_id=apt`, {
signal: controller.signal,
})
.then((r) => r.json())
.then(({ data }) => setResults(data))
."color:#c4b5fd">catch((err) => {
"color:#6b7280">// Ignore AbortError — expected when component unmounts
"color:#c4b5fd">if (err.name !== "AbortError") console.error(err);
});
"color:#c4b5fd">return () => controller.abort();
}, [query]);
"color:#c4b5fd">return <div>{/* render results */}</div>;
}