1+ // frontend/lib/api/client.ts
12import {
23 OfflineActionQueuedError ,
34 isLikelyOfflineError ,
@@ -6,22 +7,30 @@ import {
67} from '@/lib/offline' ;
78
89const API_BASE_URL =
9- process . env . NEXT_PUBLIC_API_URL || process . env . NEXT_PUBLIC_BACKEND_URL || 'http://localhost:3001/api/v1' ;
10-
11- const DEFAULT_RETRY_CONFIG : RetryConfig = {
12- maxRetries : 3 ,
13- baseDelay : 1000 ,
14- maxDelay : 10000 ,
15- jitter : true ,
16- } ;
10+ process . env . NEXT_PUBLIC_API_URL ||
11+ process . env . NEXT_PUBLIC_BACKEND_URL ||
12+ 'http://localhost:3001/api/v1' ;
1713
14+ /* =====================================================
15+ Retry Configuration
16+ ===================================================== */
1817export interface RetryConfig {
1918 maxRetries : number ;
2019 baseDelay : number ;
2120 maxDelay : number ;
2221 jitter : boolean ;
2322}
2423
24+ const DEFAULT_RETRY_CONFIG : RetryConfig = {
25+ maxRetries : 3 ,
26+ baseDelay : 1000 ,
27+ maxDelay : 10000 ,
28+ jitter : true ,
29+ } ;
30+
31+ /* =====================================================
32+ Custom API Error
33+ ===================================================== */
2534export class ApiError extends Error {
2635 status : number ;
2736 response : Response ;
@@ -36,29 +45,32 @@ export class ApiError extends Error {
3645 }
3746}
3847
48+ /** Type guard for UI handling */
49+ export function isApiError ( error : unknown ) : error is ApiError {
50+ return error instanceof ApiError ;
51+ }
52+
53+ /* =====================================================
54+ URL Resolver
55+ ===================================================== */
3956export function resolveApiUrl ( endpoint : string ) {
4057 return `${ API_BASE_URL } ${ endpoint } ` ;
4158}
4259
60+ /* =====================================================
61+ Retry Helpers
62+ ===================================================== */
4363function shouldRetryStatus ( status : number ) : boolean {
4464 return status >= 500 || status === 429 ;
4565}
4666
4767function shouldRetryError ( error : unknown ) : boolean {
48- if ( error instanceof ApiError ) {
49- return shouldRetryStatus ( error . status ) ;
50- }
68+ if ( error instanceof ApiError ) return shouldRetryStatus ( error . status ) ;
5169
52- if (
53- ( error instanceof Error && error . name === 'AbortError' ) ||
54- ( typeof error === 'object' &&
55- error !== null &&
56- 'name' in error &&
57- error . name === 'AbortError' )
58- ) {
59- return false ;
60- }
70+ // Abort should NOT retry
71+ if ( error instanceof Error && error . name === 'AbortError' ) return false ;
6172
73+ // Network errors → retry
6274 return true ;
6375}
6476
@@ -68,43 +80,35 @@ function calculateDelay(attempt: number, config: RetryConfig): number {
6880 return config . jitter ? delay * ( 0.5 + Math . random ( ) ) : delay ;
6981}
7082
71- function delay ( ms : number ) : Promise < void > {
72- return new Promise ( ( resolve ) => setTimeout ( resolve , ms ) ) ;
73- }
83+ const delay = ( ms : number ) => new Promise ( ( resolve ) => setTimeout ( resolve , ms ) ) ;
7484
85+ /* =====================================================
86+ Error Normalization
87+ ===================================================== */
7588function normalizeError ( error : unknown ) : Error {
76- if ( error instanceof Error ) {
77- return error ;
78- }
89+ if ( error instanceof Error ) return error ;
7990
8091 const message =
81- typeof error === 'object' &&
82- error !== null &&
83- 'message' in error &&
84- typeof error . message === 'string'
85- ? error . message
92+ typeof error === 'object' && error !== null && 'message' in error && typeof ( error as any ) . message === 'string'
93+ ? ( error as any ) . message
8694 : String ( error ) ;
8795
8896 const normalized = new Error ( message ) ;
8997
90- if (
91- typeof error === 'object' &&
92- error !== null &&
93- 'name' in error &&
94- typeof error . name === 'string'
95- ) {
96- normalized . name = error . name ;
98+ if ( typeof error === 'object' && error !== null && 'name' in error && typeof ( error as any ) . name === 'string' ) {
99+ normalized . name = ( error as any ) . name ;
97100 }
98101
99102 return normalized ;
100103}
101104
105+ /* =====================================================
106+ Response Parsing
107+ ===================================================== */
102108async function parseResponseBody ( response : Response ) : Promise < unknown > {
103109 const contentType = response . headers . get ( 'content-type' ) || '' ;
104110
105- if ( response . status === 204 ) {
106- return null ;
107- }
111+ if ( response . status === 204 ) return null ;
108112
109113 if ( contentType . includes ( 'application/json' ) ) {
110114 return response . json ( ) ;
@@ -114,18 +118,35 @@ async function parseResponseBody(response: Response): Promise<unknown> {
114118 return text . length > 0 ? text : null ;
115119}
116120
117- function getErrorMessage ( statusText : string , data : unknown ) : string {
118- if ( data && typeof data === 'object' && 'message' in data && typeof data . message === 'string' ) {
119- return data . message ;
121+ /* =====================================================
122+ Friendly Error Messages
123+ ===================================================== */
124+ function getErrorMessage ( status : number , statusText : string , data : unknown ) : string {
125+ if ( data && typeof data === 'object' && 'message' in data && typeof ( data as any ) . message === 'string' ) {
126+ return ( data as any ) . message ;
120127 }
121128
122- if ( typeof data === 'string' && data . trim ( ) . length > 0 ) {
123- return data ;
129+ switch ( status ) {
130+ case 400 :
131+ return 'Invalid request. Please check your input.' ;
132+ case 401 :
133+ return 'You are not authenticated. Please login again.' ;
134+ case 403 :
135+ return 'You do not have permission to perform this action.' ;
136+ case 404 :
137+ return 'Requested resource was not found.' ;
138+ case 429 :
139+ return 'Too many requests. Please try again shortly.' ;
140+ case 500 :
141+ return 'Server error. Please try again later.' ;
142+ default :
143+ return `Request failed: ${ statusText } ` ;
124144 }
125-
126- return `API Error: ${ statusText } ` ;
127145}
128146
147+ /* =====================================================
148+ Main API Call
149+ ===================================================== */
129150export async function apiCall < T = unknown > (
130151 endpoint : string ,
131152 options : RequestInit = { } ,
@@ -135,6 +156,7 @@ export async function apiCall<T = unknown>(
135156 let lastError : Error | undefined ;
136157 const shouldQueue = shouldQueueRequest ( options ) ;
137158
159+ // Queue immediately if offline
138160 if ( shouldQueue && typeof navigator !== 'undefined' && navigator . onLine === false ) {
139161 const action = queueOfflineAction ( endpoint , options ) ;
140162 throw new OfflineActionQueuedError (
@@ -150,41 +172,36 @@ export async function apiCall<T = unknown>(
150172 ...options ,
151173 headers : {
152174 'Content-Type' : 'application/json' ,
153- ...options . headers ,
175+ ...( options . headers || { } ) ,
154176 } ,
155177 } ) ;
156178
157179 const data = await parseResponseBody ( response ) ;
158180
159- if ( response . ok ) {
160- return data as T ;
161- }
181+ if ( response . ok ) return data as T ;
162182
163- throw new ApiError (
164- getErrorMessage ( response . statusText , data ) ,
165- response . status ,
166- response ,
167- data
168- ) ;
183+ throw new ApiError ( getErrorMessage ( response . status , response . statusText , data ) , response . status , response , data ) ;
169184 } catch ( error ) {
170185 lastError = normalizeError ( error ) ;
171186
187+ // Debug logging
188+ console . error ( '[API ERROR]' , { endpoint, attempt, error : lastError } ) ;
189+
190+ // Queue request if offline
172191 if ( shouldQueue && isLikelyOfflineError ( lastError ) ) {
173192 const action = queueOfflineAction ( endpoint , options ) ;
174193 throw new OfflineActionQueuedError (
175- 'The request was queued because the network is unavailable .' ,
194+ 'Network unavailable. Request queued.' ,
176195 endpoint ,
177196 action . id
178197 ) ;
179198 }
180199
181- if ( attempt === config . maxRetries || ! shouldRetryError ( error ) ) {
182- throw lastError ;
183- }
200+ if ( attempt === config . maxRetries || ! shouldRetryError ( error ) ) throw lastError ;
184201
185202 await delay ( calculateDelay ( attempt , config ) ) ;
186203 }
187204 }
188205
189206 throw lastError || new Error ( 'API call failed after retries' ) ;
190- }
207+ }
0 commit comments