Introduction
The API client (src/app/api/client.ts) provides a centralized interface for making HTTP requests to the backend. It includes built-in support for authentication, retries, timeouts, and error handling.
Core Functions
The API client exports five main functions:
request() Core request function with retry and timeout logic
apiGET() Wrapper for GET requests
apiPOST() Wrapper for POST requests
apiPATCH() Wrapper for PATCH requests
apiDELETE() Wrapper for DELETE requests
Request Function
The request() function is the foundation of the API client:
import { useAuthStore } from "../../app/store/auth/auth.store" ;
const API_BASE_URL = import . meta . env . VITE_API_URL ;
export async function request (
endpoint : string ,
options : RequestInit & { retry ?: number , timeOut ?: number } = {}
) {
const headers : HeadersInit = {
... ( options . body ? { "Content-Type" : "application/json" } : {}),
... options . headers ,
}
// Separate retry, timeout and request headers
const { retry = 0 , timeOut = 1000 * 30 , ... fetchOptions } = options
const controller = new AbortController ()
const timeoutId = setTimeout (() => controller . abort (), timeOut )
try {
const response = await fetch ( ` ${ API_BASE_URL }${ endpoint } ` , {
... fetchOptions ,
credentials: "include" ,
headers ,
signal: controller . signal
})
if ( response . status === 401 ) {
useAuthStore . getState (). logout ();
throw new Error ( "Unauthorized" );
}
if ( ! response . ok ) {
let messsage = "API request failed"
try {
const error = await response . json (). catch (() => ({}))
messsage = error . error || messsage
} catch { }
throw new Error ( messsage )
}
return response . status === 204 ? null : response . json ();
} catch ( error ) {
if ( error instanceof DOMException && error . name === "AbortError" ) {
throw new Error ( "Request timeout" )
}
// Retry if error and ignore when the error is 401 or 403
if ( retry > 0 && ! ( error instanceof Error &&
( error . message === "Unauthorized" || error . message === "Forbidden" ))) {
return request ( endpoint , { ... options , retry: retry - 1 })
}
throw error
} finally {
clearTimeout ( timeoutId )
}
}
HTTP Method Wrappers
Convenience functions wrap the request() function:
export function apiGET ( url : string , options : RequestInit = {}) {
return request ( url , { ... options , method: "GET" })
}
export function apiPOST ( url : string , options : RequestInit = {}) {
return request ( url , { ... options , method: "POST" })
}
export function apiDELETE ( url : string , options : RequestInit = {}) {
return request ( url , { ... options , method: "DELETE" })
}
export function apiPATCH ( url : string , options : RequestInit = {}) {
return request ( url , { ... options , method: "PATCH" })
}
Usage Examples
GET Request
import { apiGET } from '@app/api/client' ;
const user = await apiGET ( '/auth/me' );
console . log ( user );
POST Request with Body
import { apiPOST } from '@app/api/client' ;
const response = await apiPOST ( '/auth/login' , {
body: JSON . stringify ({
email: '[email protected] ' ,
password: 'password123'
})
});
PATCH Request
import { apiPATCH } from '@app/api/client' ;
const updated = await apiPATCH ( '/users/123' , {
body: JSON . stringify ({
name: 'New Name'
})
});
DELETE Request
import { apiDELETE } from '@app/api/client' ;
await apiDELETE ( '/items/456' );
Request with Custom Options
import { apiGET } from '@app/api/client' ;
const data = await apiGET ( '/slow-endpoint' , {
retry: 3 , // Retry up to 3 times
timeOut: 10000 , // 10 second timeout
headers: {
'X-Custom-Header' : 'value'
}
});
Key Features
Automatic Authentication
Include Credentials
All requests include credentials: "include" to send cookies automatically.
Handle 401 Responses
When the server returns 401 Unauthorized, the client automatically logs out the user.
Clear Session
The auth store is updated to clear user session data (src/app/api/client.ts:28).
if ( response . status === 401 ) {
useAuthStore . getState (). logout ();
throw new Error ( "Unauthorized" );
}
Timeout Handling
Requests have a configurable timeout (default 30 seconds):
const controller = new AbortController ()
const timeoutId = setTimeout (() => controller . abort (), timeOut )
try {
const response = await fetch ( url , {
signal: controller . signal
});
} finally {
clearTimeout ( timeoutId )
}
Timeouts use the AbortController API to cancel in-flight requests, preventing memory leaks.
Retry Logic
Failed requests can be automatically retried:
if ( retry > 0 && ! ( error instanceof Error &&
( error . message === "Unauthorized" || error . message === "Forbidden" ))) {
return request ( endpoint , { ... options , retry: retry - 1 })
}
Retry behavior:
Retries occur when retry > 0 is specified
Does not retry on 401 (Unauthorized) or 403 (Forbidden)
Does not retry on timeout errors
Decrements retry count on each attempt
Retries are not performed for authentication errors (401, 403) to prevent loops and account lockouts.
Error Handling
The client provides detailed error messages:
if ( ! response . ok ) {
let messsage = "API request failed"
try {
const error = await response . json (). catch (() => ({}))
messsage = error . error || messsage
} catch { }
throw new Error ( messsage )
}
Check Response Status
Verify response.ok (status 200-299).
Parse Error Message
Attempt to extract error message from response body.
Fallback Message
Use generic message if parsing fails.
Throw Error
Throw error for calling code to handle.
204 No Content Handling
Requests returning 204 No Content return null:
return response . status === 204 ? null : response . json ();
Configuration
Base URL
The API base URL is configured via environment variable:
const API_BASE_URL = import . meta . env . VITE_API_URL ;
.env.development
.env.production
VITE_API_URL = http://localhost:8000/api
VITE_API_URL = https://api.example.com
Headers are automatically set based on request content:
const headers : HeadersInit = {
... ( options . body ? { "Content-Type" : "application/json" } : {}),
... options . headers ,
}
Content-Type: application/json is added when body is present
Custom headers can be passed via options.headers
Cookie credentials are automatically included
Error Types
The API client throws different error types:
throw new Error ( "Request timeout" )
Thrown when request exceeds the timeout duration.
throw new Error ( "Unauthorized" )
Thrown on 401 status. Automatically logs out user.
throw new Error ( "API request failed" )
Generic error with optional server-provided message.
Native fetch errors (network failure, CORS, etc.)
Usage in Stores
The API client is commonly used in Zustand stores:
import { create } from "zustand" ;
import { apiGET , apiPOST } from "@app/api/client" ;
export const useDataStore = create (( set ) => ({
data: null ,
loading: false ,
fetchData : async () => {
set ({ loading: true });
try {
const data = await apiGET ( '/data' );
set ({ data , loading: false });
} catch ( error ) {
set ({ loading: false });
console . error ( 'Failed to fetch data:' , error );
}
},
createItem : async ( item ) => {
const created = await apiPOST ( '/items' , {
body: JSON . stringify ( item )
});
return created ;
},
}));
Usage in Services
Feature services use the API client for domain-specific operations:
// src/features/auth/services/auth.service.ts
import { apiPOST } from '@app/api/client' ;
export async function loginUser ( credentials : LoginCredentials ) {
return apiPOST ( '/auth/login' , {
body: JSON . stringify ( credentials ),
retry: 1 ,
timeOut: 10000 ,
});
}
export async function registerUser ( data : RegisterData ) {
return apiPOST ( '/auth/register' , {
body: JSON . stringify ( data )
});
}
Best Practices
Keep all API interactions in the client or feature services - never use fetch() directly in components.
Always wrap API calls in try/catch blocks and provide user feedback.
Define TypeScript types for request/response data.
Set appropriate timeouts for slow endpoints (file uploads, reports, etc.).
Leverage Retry for Stability
Use retry for transient failures but not for user errors (4xx responses).
Advanced Usage
await apiGET ( '/protected' , {
headers: {
'Authorization' : `Bearer ${ token } ` ,
'X-Custom-Header' : 'value'
}
});
File Uploads
const formData = new FormData ();
formData . append ( 'file' , file );
await apiPOST ( '/upload' , {
body: formData ,
// Don't set Content-Type - browser will set it with boundary
headers: {},
timeOut: 60000 , // 60 second timeout for large files
});
Abort Requests
const controller = new AbortController ();
// Start request
const promise = apiGET ( '/slow-endpoint' , {
signal: controller . signal
});
// Cancel after 5 seconds
setTimeout (() => controller . abort (), 5000 );
try {
await promise ;
} catch ( error ) {
console . log ( 'Request cancelled' );
}
State Management Learn how API client is used in Zustand stores
Architecture Overview Understand the overall architecture
Fetch API MDN documentation for the Fetch API