Skip to content

Instantly share code, notes, and snippets.

@marvinkome
Created October 21, 2025 15:39
Show Gist options
  • Select an option

  • Save marvinkome/d2661f42cc629b13cde53407d1609ec1 to your computer and use it in GitHub Desktop.

Select an option

Save marvinkome/d2661f42cc629b13cde53407d1609ec1 to your computer and use it in GitHub Desktop.
import axios, { AxiosInstance, AxiosResponse } 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 | undefined>>()
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 | undefined> => {
const lockKey = 'access_token_refresh'
const cachedFn = tokenRefreshLock.get(lockKey)
if (cachedFn) return cachedFn
const refreshPromise = (async () => {
try {
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 undefined
}
// 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')
await clearTokens()
return undefined
}
await storeTokens(access_token, newRefreshToken)
return access_token
} catch (err) {
console.error('Token generation error: Unknown error occurred', err)
return undefined
} finally {
tokenRefreshLock.delete(lockKey)
}
})()
tokenRefreshLock.set(lockKey, refreshPromise)
return refreshPromise
}
// -----------------------------------------------------------------------------
// Generic API Caller with Retry + Exponential Backoff
// -----------------------------------------------------------------------------
export const callApi = async <T>(
endpoint: string,
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
data?: Record<string, unknown> | FormData,
options: { retryCount?: number; forceRenew?: boolean } = {}
): Promise<T> => {
const { retryCount = 3, forceRenew = false } = options
const cancelTokenSource = axios.CancelToken.source()
try {
const token = await getAccessToken(forceRenew)
if (!token) throw new Error('Unable to obtain access token')
const response: AxiosResponse<T> = await api.request({
url: endpoint,
method,
...(data && { data }),
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
cancelToken: cancelTokenSource.token,
})
return response.data
} catch (error: unknown) {
const message = err?.response?.data?.message || err.message || 'Unknown error'
console.error(`API error: ${message}`)
// Retry on token expiry or unauthorized errors
// going to leave this here. But ideally you can just check for status code 401
// 403 usually doesn't mean an access token issue, but rather a permission issue
if (/unauthorized|expired|forbidden/i.test(message) && retryCount > 1) {
console.warn(`Token expired — retrying (${retryCount - 1} retries left)`)
// Exponential backoff (1s, 2s, 4s)
await new Promise((r) => setTimeout(r, Math.pow(2, 3 - retryCount) * 1000))
return callApi(endpoint, method, data, { retryCount: retryCount - 1, forceRenew: true })
}
// in the case that max retries is reached, we want to throw the error
// instead of throwing a new error. The caller doesn't know or care about the token ret
throw error
} finally {
cancelTokenSource.cancel('Operation completed')
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment