Skip to content

Instantly share code, notes, and snippets.

@lordsarcastic
Forked from marvinkome/callApi.ts
Last active October 21, 2025 18:20
Show Gist options
  • Select an option

  • Save lordsarcastic/ce608a3df278e036b39dd456d47402a7 to your computer and use it in GitHub Desktop.

Select an option

Save lordsarcastic/ce608a3df278e036b39dd456d47402a7 to your computer and use it in GitHub Desktop.
import axios, { AxiosInstance, AxiosResponse, AxiosError } from 'axios'
// -----------------------------------------------------------------------------
// Constants
// -----------------------------------------------------------------------------
const TOKEN_EXPIRY = {
ACCESS_TOKEN: 1800,
REFRESH_TOKEN: 2592000,
} as const
const CACHE_KEYS = {
ACCESS_TOKEN: 'app:access_token',
REFRESH_TOKEN: 'app:refresh_token',
} as const
// -----------------------------------------------------------------------------
// Simple In-Memory Cache (swap out for Redis/localStorage if needed)
// -----------------------------------------------------------------------------
const memoryCache = new Map<string, { value: string; expires: number }>()
const getFromMemory = (key: string) => {
const entry = memoryCache.get(key)
if (!entry) return null
if (Date.now() > entry.expires) {
memoryCache.delete(key)
return null
}
return entry.value
}
const setInMemory = (key: string, value: string, ttlSeconds: number) => {
memoryCache.set(key, {
value,
expires: Date.now() + ttlSeconds * 1000,
})
}
const delFromMemory = (key: string) => memoryCache.delete(key)
// -----------------------------------------------------------------------------
// Configuration
// -----------------------------------------------------------------------------
const CONFIG = {
BASE_URL: process.env.API_BASE_URL || 'https://api.example.com',
}
// -----------------------------------------------------------------------------
// API Instance
// -----------------------------------------------------------------------------
const api: AxiosInstance = axios.create({
baseURL: CONFIG.BASE_URL,
timeout: 30000,
timeoutErrorMessage: 'Request timed out',
})
// -----------------------------------------------------------------------------
// Token Management
// -----------------------------------------------------------------------------
const tokenRefreshLock = new Map<string, Promise<string | null>>()
const storeTokens = async (accessToken: string, refreshToken?: string) => {
setInMemory(CACHE_KEYS.ACCESS_TOKEN, accessToken, TOKEN_EXPIRY.ACCESS_TOKEN)
if (refreshToken) {
setInMemory(CACHE_KEYS.REFRESH_TOKEN, refreshToken, TOKEN_EXPIRY.REFRESH_TOKEN)
}
}
const clearTokens = async () => {
delFromMemory(CACHE_KEYS.ACCESS_TOKEN)
delFromMemory(CACHE_KEYS.REFRESH_TOKEN)
}
/**
* Returns a valid access token or undefined if we can't get one.
* Automatically refreshes if expired, with a concurrency lock.
*/
export const getAccessToken = async (forceRenew = false): Promise<string | null> => {
const lockKey = 'access_token_refresh'
const cachedFn = tokenRefreshLock.get(lockKey) ?? null;
if (cachedFn) return cachedFn
const refreshPromise = (async () => {
const cachedAccess = getFromMemory(CACHE_KEYS.ACCESS_TOKEN)
if (cachedAccess && !forceRenew) return cachedAccess
const refreshToken = getFromMemory(CACHE_KEYS.REFRESH_TOKEN)
if (!refreshToken) {
console.error('Token generation error: No refresh token found')
return null;
}
// Replace this with your real refresh endpoint
const response = await api.post('/auth/token', { refresh_token: refreshToken })
const { access_token, refresh_token: newRefreshToken } = response.data || {}
if (!access_token) {
console.error('Token generation error: No access token found')
return null;
}
if (!response.ok) {
console.error('Token generation error: Failed to refresh token', response.statusText)
return null;
}
await storeTokens(access_token, newRefreshToken)
tokenRefreshLock.delete(lockKey)
return access_token
})()
tokenRefreshLock.set(lockKey, refreshPromise)
return refreshPromise
}
const makeRequest = async <T>(
endpoint: string,
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
data: Record<string, unknown> | FormData | undefined,
token: string,
): Promise<AxiosResponse<T>> => {
const response: AxiosResponse<T> = await api.request({
url: endpoint,
method,
...(data && { data }),
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
signal: AbortSignal.timeout(5000),
})
}
// -----------------------------------------------------------------------------
// Generic API Caller with Retry + Exponential Backoff
// -----------------------------------------------------------------------------
const ACCESS_TOKEN_RETRIEVAL_ERROR = 'Unable to obtain access token'
const SERVER_ERROR = 'Server error'
enum Result {
Ok,
Err
}
type Failable<T, E> = {
result: Result,
body?: T | E,
};
export const callApi = async <T>(
endpoint: string,
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
data?: Record<string, unknown> | FormData,
options: { maxRetries?: number; forceRenew?: boolean } = {}
): Promise<Failable<T, AxiosError>> => {
const { maxRetries = 3, forceRenew = false } = options
const getToken = async (): Promise<Failable<string, AxiosError>> => {
let token = await getAccessToken(forceRenew);
if (!token) return {
result: Result.Err,
body: new Error(ACCESS_TOKEN_RETRIEVAL_ERROR) as AxiosError
}
return {
result: Result.Ok,
body: token
};
}
let tokenCall = await getToken();
let token: string;
if (tokenCall.result === Result.Err) {
return {
result: Result.Err,
body: tokenCall.body
}
} else {
token = tokenCall.body as string;
}
let response: AxiosResponse<T>;
let retryCount = maxRetries;
const cancelTokenSource = axios.CancelToken.source()
while (retryCount > 0) {
response = await makeRequest<T>(endpoint, method, data, token);
const message = response?.data?.message || 'Unknown error'
if (response.status >= 200 && response.status < 300) {
return response.data;
} else if (response.status === 401) {
let tokenCall = await getToken();
if (tokenCall.result === Result.Err) {
return {
result: Result.Err,
body: tokenCall.body
}
} else {
token = tokenCall.body as string;
}
} else if (response.status > 401 && response.status < 500) {
return {
result: Result.Err,
body: new Error(message) as AxiosError
}
}
console.warn(`API call failed: ${message} — retrying (${retryCount - 1} retries left)`)
retryCount--;
await new Promise((r) => setTimeout(r, Math.pow(2, 3 - retryCount) * 1000))
}
return {
result: Result.Err,
body: new Error(SERVER_ERROR) as AxiosError
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment