api-client.ts 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. /**
  2. * API Client Service
  3. *
  4. * Handles all HTTP communication with the admin backend API.
  5. */
  6. import axios, { AxiosError, AxiosInstance } from 'axios';
  7. import { API_BASE_URL, API_TIMEOUT, HttpMethod } from '../constants.js';
  8. import type { ApiError } from '../types.js';
  9. /**
  10. * API Client class for making HTTP requests to the backend
  11. */
  12. export class ApiClient {
  13. private client: AxiosInstance;
  14. private token: string | null = null;
  15. constructor(baseURL: string = API_BASE_URL) {
  16. this.client = axios.create({
  17. baseURL,
  18. timeout: API_TIMEOUT,
  19. headers: {
  20. 'Content-Type': 'application/json',
  21. 'Accept': 'application/json'
  22. }
  23. });
  24. // Request interceptor to add auth token
  25. this.client.interceptors.request.use((config) => {
  26. if (this.token) {
  27. config.headers.Authorization = `Bearer ${this.token}`;
  28. }
  29. return config;
  30. });
  31. // Response interceptor for error handling
  32. this.client.interceptors.response.use(
  33. (response) => response,
  34. (error) => {
  35. throw this.handleError(error);
  36. }
  37. );
  38. }
  39. /**
  40. * Set authentication token
  41. */
  42. setToken(token: string): void {
  43. this.token = token;
  44. }
  45. /**
  46. * Get current authentication token
  47. */
  48. getToken(): string | null {
  49. return this.token;
  50. }
  51. /**
  52. * Clear authentication token
  53. */
  54. clearToken(): void {
  55. this.token = null;
  56. }
  57. /**
  58. * Make a generic API request
  59. */
  60. async request<T>(
  61. method: HttpMethod,
  62. endpoint: string,
  63. data?: unknown,
  64. params?: Record<string, unknown>
  65. ): Promise<T> {
  66. try {
  67. const response = await this.client.request<T>({
  68. method,
  69. url: endpoint,
  70. data,
  71. params
  72. });
  73. return response.data;
  74. } catch (error) {
  75. throw this.handleError(error);
  76. }
  77. }
  78. /**
  79. * Make a GET request
  80. */
  81. async get<T>(endpoint: string, params?: Record<string, unknown>): Promise<T> {
  82. return this.request<T>('GET', endpoint, undefined, params);
  83. }
  84. /**
  85. * Make a POST request
  86. */
  87. async post<T>(endpoint: string, data?: unknown): Promise<T> {
  88. return this.request<T>('POST', endpoint, data);
  89. }
  90. /**
  91. * Make a PUT request
  92. */
  93. async put<T>(endpoint: string, data?: unknown): Promise<T> {
  94. return this.request<T>('PUT', endpoint, data);
  95. }
  96. /**
  97. * Make a DELETE request
  98. */
  99. async delete<T>(endpoint: string): Promise<T> {
  100. return this.request<T>('DELETE', endpoint);
  101. }
  102. /**
  103. * Handle API errors and return formatted error messages
  104. */
  105. private handleError(error: unknown): Error {
  106. if (axios.isAxiosError(error)) {
  107. const axiosError = error as AxiosError<ApiError>;
  108. if (axiosError.response) {
  109. const { status, data } = axiosError.response;
  110. switch (status) {
  111. case 400:
  112. return new Error(
  113. `Error: ${data?.message || 'Invalid request parameters'}. ` +
  114. 'Please check your input and try again.'
  115. );
  116. case 401:
  117. return new Error(
  118. 'Error: Authentication failed. ' +
  119. 'Please check your credentials or re-login.'
  120. );
  121. case 403:
  122. return new Error(
  123. 'Error: Permission denied. ' +
  124. 'You do not have access to this resource.'
  125. );
  126. case 404:
  127. return new Error(
  128. 'Error: Resource not found. ' +
  129. 'Please check the ID is correct.'
  130. );
  131. case 429:
  132. return new Error(
  133. 'Error: Rate limit exceeded. ' +
  134. 'Please wait before making more requests.'
  135. );
  136. case 500:
  137. return new Error(
  138. `Error: Server error. ${data?.message || 'Please try again later.'}`
  139. );
  140. default:
  141. return new Error(
  142. `Error: API request failed with status ${status}. ` +
  143. (data?.message || '')
  144. );
  145. }
  146. } else if (axiosError.code === 'ECONNABORTED') {
  147. return new Error(
  148. 'Error: Request timed out. ' +
  149. 'Please try again or check your network connection.'
  150. );
  151. } else if (axiosError.code === 'ECONNREFUSED') {
  152. return new Error(
  153. `Error: Cannot connect to API server at ${this.client.defaults.baseURL}. ` +
  154. 'Please ensure the server is running.'
  155. );
  156. }
  157. }
  158. return new Error(
  159. `Error: Unexpected error occurred: ${error instanceof Error ? error.message : String(error)}`
  160. );
  161. }
  162. }
  163. /**
  164. * Singleton API client instance
  165. */
  166. let apiClientInstance: ApiClient | null = null;
  167. /**
  168. * Get or create the singleton API client instance
  169. */
  170. export function getApiClient(): ApiClient {
  171. if (!apiClientInstance) {
  172. apiClientInstance = new ApiClient();
  173. }
  174. return apiClientInstance;
  175. }
  176. /**
  177. * Reset the singleton API client instance (useful for testing)
  178. */
  179. export function resetApiClient(): void {
  180. apiClientInstance = null;
  181. }