Skip to content

Commit b9ceb3a

Browse files
authored
feat: add FetchClient and related types for improved API handling (#33)
2 parents 5a3a144 + decd9fa commit b9ceb3a

File tree

11 files changed

+521
-86
lines changed

11 files changed

+521
-86
lines changed

.github/copilot-instructions.md

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
1-
1. Always use latest React 19 version and TypeScript for our ReactJS development. Always show examples and reference the latest version of React in your responses. If in doubt, use docs from https://react.dev/learn
2-
2. Always use React router, MUI latest components for React development
3-
3. Always use functional components and hooks for state management in React
4-
4. Always use TypeScript for type safety in React components
5-
5. Always use the latest best practices for performance optimization in React, such as memoization and lazy loading
6-
6. Always ensure accessibility standards are met in React components
7-
7. Always use ESLint and Prettier for code formatting and linting in React projects
8-
8. Always provide clear and concise comments in the code to explain complex logic or important decisions
9-
9. Always use PropTypes or TypeScript interfaces for defining component props
10-
10. Always ensure that components are reusable and modular, following the DRY (Don't Repeat Yourself) principle
1+
# Copilot Instructions for the Codebase
2+
3+
## Overview
4+
5+
- **React Kit** is a React/TypeScript library
6+
- The app uses Vite for build/dev, Playwright for E2E tests, and Vitest for unit tests. State management is via Jotai, data fetching with React Query, and UI with MUI
7+
8+
## React & TypeScript Best Practices
9+
10+
1. Always use the latest React 19 version and TypeScript for ReactJS development. Reference the latest React docs at https://react.dev/learn.
11+
2. Use React Router and the latest MUI components for all routing and UI needs.
12+
3. Write only functional components and use React hooks for all state management.
13+
4. Use TypeScript for type safety in all React components and logic.
14+
5. Apply the latest React performance optimizations (e.g., memoization, lazy loading, code splitting).
15+
6. Ensure all components meet accessibility (a11y) standards.
16+
7. Use ESLint and Prettier for code formatting and linting. Follow project configs.
17+
8. Add clear, concise comments to explain complex logic or important decisions.
18+
9. Define component props using TypeScript interfaces (preferred) or PropTypes.
19+
10. Design components to be reusable and modular, following DRY principles.
20+
21+
---
1122

apps/react-kit-demo/src/services/BookService.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { Book } from '../types/Book';
2-
import { fetchClient } from '@react-kit/*';
1+
import { FetchClient } from '@react-kit/*';
32
import { BASE_API_URL, BOOK_API_URL } from '../constants/ApiConstants';
3+
import { Book } from '../types/Book';
44

55
/**
66
* Utility class for Books operations
@@ -16,6 +16,6 @@ export class BookService {
1616
* @since 1.0.0
1717
*/
1818
static async getAllBooks(): Promise<Book[]> {
19-
return await fetchClient<Book[]>(`${BASE_API_URL + BOOK_API_URL}/books`);
19+
return await FetchClient<Book[]>(`${BASE_API_URL + BOOK_API_URL}/books`);
2020
}
2121
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"update": "nx migrate latest"
1414
},
1515
"dependencies": {
16+
"@axa-fr/react-oidc": "^7.25.14",
1617
"@emotion/react": "^11.14.0",
1718
"@emotion/styled": "^11.14.1",
1819
"@mui/icons-material": "^7.2.0",

pnpm-lock.yaml

Lines changed: 26 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

react-kit/src/index.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,41 @@
11
// Export all buttons
22
export { CancelButton } from './lib/components/buttons/CancelButton';
33
export { DeleteButton } from './lib/components/buttons/DeleteButton';
4+
export { EditIconButton } from './lib/components/buttons/EditIconButton';
5+
export { ExcelButton } from './lib/components/buttons/ExcelButton';
46
export { GoBackButton } from './lib/components/buttons/GoBackButton';
57
export { HistoryButton } from './lib/components/buttons/HistoryButton';
68
export { LoadingSuccessButton } from './lib/components/buttons/LoadingSuccessButton';
79
export { ManageButton } from './lib/components/buttons/ManageButton';
810
export { SuccessButton } from './lib/components/buttons/SuccessButton';
9-
export { ExcelButton } from './lib/components/buttons/ExcelButton';
10-
export { EditIconButton } from './lib/components/buttons/EditIconButton';
1111

1212
// Export snackbar components
1313
export { AppSnackBar } from './lib/components/snack-bar/AppSnackBar';
1414
export { QuerySnackBar } from './lib/components/snack-bar/QuerySnackBar';
1515

1616
// Export all other components
17-
export { TablePaginationActions } from './lib/components/table/TablePaginationActions';
18-
export { TabPanel, a11yProps } from './lib/components/tabs/TabPanel';
19-
export { NextLink } from './lib/components/NextLink';
2017
export { CenteredCircularProgress } from './lib/components/CenteredCircularProgress';
2118
export { ConfirmDialog } from './lib/components/ConfirmationDialog';
2219
export { DismissibleAlert } from './lib/components/DismissibleAlert';
20+
export { NextLink } from './lib/components/NextLink';
2321
export { OpenInNewIconLink } from './lib/components/OpenInNewIconLink';
2422
export { ReactIf } from './lib/components/ReactIf';
23+
export { TablePaginationActions } from './lib/components/table/TablePaginationActions';
24+
export { a11yProps, TabPanel } from './lib/components/tabs/TabPanel';
25+
26+
// Export fetch client
27+
export { FetchClient as fetchClient, FetchClient, FetchClient as httpClient } from './lib/config/fetch/FetchClient';
28+
export * from './lib/config/fetch/FetchClientTypes';
29+
export { FetchInterceptor } from './lib/config/fetch/FetchInterceptor';
2530

2631
// Export all utilities
2732
export * from './lib/utils/BooleanUtils';
33+
export * from './lib/utils/CssUtils';
2834
export * from './lib/utils/DateUtils';
2935
export * from './lib/utils/NumberUtils';
3036
export * from './lib/utils/ProgressStateUtils';
3137
export * from './lib/utils/StringUtils';
3238
export * from './lib/utils/UrlUtils';
33-
export * from './lib/utils/fetchClient';
34-
export * from './lib/utils/CssUtils';
3539

3640
// Export all types
3741
export * from './lib/types/ProgressState';

react-kit/src/lib/components/buttons/EditIconButton.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import React from 'react';
2-
import { IconButton, Tooltip } from '@mui/material';
31
import EditIcon from '@mui/icons-material/Edit';
2+
import { IconButton, Tooltip } from '@mui/material';
3+
import React from 'react';
44

55
interface EditIconButtonProps {
66
tooltipTitle: string;
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import { FetchError, FetchResponse, RequestConfig } from './FetchClientTypes';
2+
import { createFormattedError } from './FetchInterceptor';
3+
4+
/**
5+
* Build URL with query parameters
6+
* @param baseURL Base URL
7+
* @param url Endpoint URL
8+
* @param params Query parameters
9+
* @returns Complete URL with query parameters
10+
*/
11+
const buildURL = (baseURL: string, url: string, params?: Record<string, string | number | boolean>): string => {
12+
const fullURL = url.startsWith('http') ? url : `${baseURL}${url}`;
13+
14+
if (!params) return fullURL;
15+
16+
const urlObj = new URL(fullURL);
17+
Object.entries(params).forEach(([key, value]) => {
18+
if (value !== undefined && value !== null) {
19+
urlObj.searchParams.append(key, String(value));
20+
}
21+
});
22+
23+
return urlObj.toString();
24+
};
25+
26+
/**
27+
* Parse response data based on content type
28+
* @param response Fetch response
29+
* @returns Parsed data
30+
*/
31+
const parseResponseData = async (response: Response): Promise<unknown> => {
32+
const contentType = response.headers.get('content-type');
33+
34+
if (contentType?.includes('application/json')) {
35+
return await response.json();
36+
} else if (contentType?.includes('text/')) {
37+
return await response.text();
38+
} else {
39+
return await response.blob();
40+
}
41+
};
42+
43+
/**
44+
* Reusable fetch client class
45+
*
46+
* @author Pavan Kumar Jadda
47+
* @since 0.2.19
48+
*/
49+
class FetchInstance {
50+
private readonly baseURL: string;
51+
private requestInterceptors: Array<(config: RequestConfig) => RequestConfig | Promise<RequestConfig>> = [];
52+
private responseInterceptors: Array<(response: FetchResponse<unknown>) => FetchResponse<unknown> | Promise<FetchResponse<unknown>>> = [];
53+
private errorInterceptors: Array<(error: FetchError) => FetchError | Promise<FetchError> | void | Promise<void>> = [];
54+
/**
55+
* Add request interceptor
56+
*/
57+
interceptors = {
58+
request: {
59+
use: (interceptor: (config: RequestConfig) => RequestConfig | Promise<RequestConfig>) => {
60+
this.requestInterceptors.push(interceptor);
61+
},
62+
},
63+
response: {
64+
use: (
65+
successInterceptor?: (response: FetchResponse<unknown>) => FetchResponse<unknown> | Promise<FetchResponse<unknown>>,
66+
errorInterceptor?: (error: FetchError) => FetchError | Promise<FetchError> | void | Promise<void>
67+
) => {
68+
if (successInterceptor) {
69+
this.responseInterceptors.push(successInterceptor);
70+
}
71+
if (errorInterceptor) {
72+
this.errorInterceptors.push(errorInterceptor);
73+
}
74+
},
75+
},
76+
};
77+
78+
constructor(baseURL: string = import.meta.env.VITE_REACT_APP_BASE_URL) {
79+
this.baseURL = baseURL;
80+
}
81+
82+
/**
83+
* GET request
84+
*/
85+
async get<T = unknown>(url: string, config?: Omit<RequestConfig, 'method' | 'url'>): Promise<FetchResponse<T>> {
86+
return this.executeRequest({ ...config, method: 'GET', url });
87+
}
88+
89+
/**
90+
* POST request
91+
*/
92+
async post<T = unknown>(url: string, data?: unknown, config?: Omit<RequestConfig, 'method' | 'url' | 'body'>): Promise<FetchResponse<T>> {
93+
const body = data ? JSON.stringify(data) : undefined;
94+
return this.executeRequest({ ...config, method: 'POST', url, body });
95+
}
96+
97+
/**
98+
* PUT request
99+
*/
100+
async put<T = unknown>(url: string, data?: unknown, config?: Omit<RequestConfig, 'method' | 'url' | 'body'>): Promise<FetchResponse<T>> {
101+
const body = data ? JSON.stringify(data) : undefined;
102+
return this.executeRequest({ ...config, method: 'PUT', url, body });
103+
}
104+
105+
/**
106+
* DELETE request
107+
*/
108+
async delete<T = unknown>(url: string, config?: Omit<RequestConfig, 'method' | 'url'>): Promise<FetchResponse<T>> {
109+
return this.executeRequest({ ...config, method: 'DELETE', url });
110+
}
111+
112+
/**
113+
* PATCH request
114+
*/
115+
async patch<T = unknown>(
116+
url: string,
117+
data?: unknown,
118+
config?: Omit<RequestConfig, 'method' | 'url' | 'body'>
119+
): Promise<FetchResponse<T>> {
120+
const body = data ? JSON.stringify(data) : undefined;
121+
return this.executeRequest<T>({ ...config, method: 'PATCH', url, body });
122+
}
123+
124+
/**
125+
* Generic request method
126+
*/
127+
async request<T = unknown>(config: RequestConfig): Promise<FetchResponse<T>> {
128+
return this.executeRequest(config);
129+
}
130+
131+
/**
132+
* Execute request with interceptors
133+
*/
134+
private async executeRequest<T>(config: RequestConfig): Promise<FetchResponse<T>> {
135+
// Apply request interceptors
136+
let processedConfig = config;
137+
for (const interceptor of this.requestInterceptors) {
138+
processedConfig = await interceptor(processedConfig);
139+
}
140+
141+
const { url, params, ...fetchConfig } = processedConfig;
142+
const fullURL = buildURL(this.baseURL, url || '', params);
143+
144+
try {
145+
const response = await fetch(fullURL, fetchConfig);
146+
const data = await parseResponseData(response);
147+
148+
const fetchResponse: FetchResponse<T> = {
149+
data: data as T,
150+
status: response.status,
151+
statusText: response.statusText,
152+
headers: response.headers,
153+
config: processedConfig,
154+
};
155+
156+
// Apply response interceptors
157+
let processedResponse = fetchResponse;
158+
for (const interceptor of this.responseInterceptors) {
159+
processedResponse = (await interceptor(processedResponse)) as FetchResponse<T>;
160+
}
161+
162+
return processedResponse;
163+
} catch (error) {
164+
// Apply error interceptors
165+
let processedError = createFormattedError(error as Error, 0, 'Network error. Please check your connection and try again.');
166+
for (const interceptor of this.errorInterceptors) {
167+
const result = await interceptor(processedError);
168+
if (result !== undefined) {
169+
processedError = result;
170+
}
171+
}
172+
173+
throw processedError;
174+
}
175+
}
176+
}
177+
178+
/**
179+
* Create fetch client instance
180+
*/
181+
const fetchInstance = new FetchInstance();
182+
183+
/**
184+
* Callable fetch client that defaults to GET requests
185+
*/
186+
export const FetchClient = Object.assign(async <T = unknown>(url: string, config?: Omit<RequestConfig, 'method' | 'url'>): Promise<T> => {
187+
const response = await fetchInstance.request<T>({ ...config, method: 'GET', url });
188+
return response.data;
189+
}, fetchInstance);
190+
191+
// Export the client instance for direct use
192+
export default FetchClient;
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* Interface for error response structure
3+
*/
4+
export interface ErrorResponse {
5+
message: string;
6+
errorCode: string | null;
7+
status: string;
8+
statusCode: number;
9+
timestamp: string;
10+
errors: unknown[] | null;
11+
path: string | null;
12+
}
13+
14+
/**
15+
* Interface for request configuration
16+
*/
17+
export interface RequestConfig extends RequestInit {
18+
baseURL?: string;
19+
url?: string;
20+
params?: Record<string, string | number | boolean>;
21+
}
22+
23+
/**
24+
* Interface for response structure
25+
*/
26+
export interface FetchResponse<T = unknown> {
27+
data: T;
28+
status: number;
29+
statusText: string;
30+
headers: Headers;
31+
config: RequestConfig;
32+
}
33+
34+
/**
35+
* Interface for error structure
36+
*/
37+
export interface FetchError extends Error {
38+
statusCode: number;
39+
originalError: Error;
40+
response?: Response;
41+
request?: Request;
42+
}

0 commit comments

Comments
 (0)