@gvray/request is a universal HTTP client built on a dual-engine architecture (Axios + Fetch) with a declarative preset system and a rich set of composable interceptors. It handles the hard parts — auth token refresh, retry with backoff, response caching, structured logging — so you can focus on your application logic.
- Dual engine — switch between Axios and native Fetch per-instance
- Preset system — configure complex behaviors declaratively with zero boilerplate
- Smart token refresh — proactive (request-side) and reactive (response-side) strategies
- Auto retry — exponential backoff, custom conditions, status code filtering
- Response caching — pluggable storage, TTL, per-request bypass
- Structured logging — configurable log levels, custom loggers, request timing
- Two usage patterns — global singleton or independent instances
- Fully typed — strict TypeScript, generic response types, no
anyleakage - Composable interceptors — use presets or compose interceptors manually
npm install @gvray/request
# or
pnpm add @gvray/request
# or
yarn add @gvray/requestimport{createClient,request}from'@gvray/request';importstoretifyfrom'storetify';createClient({baseURL: 'https://api.example.com',preset: {bearerAuth: {getToken: ()=>storetify<string>('access_token'),},},});constusers=awaitrequest<User[]>('/users');Initialize once, use request anywhere in your app. Ideal for most applications with a single API base URL.
import{createClient,request,requestSafe}from'@gvray/request';importstoretifyfrom'storetify';createClient({baseURL: 'https://api.example.com',timeout: 10000,preset: {bearerAuth: {getToken: ()=>storetify<string>('access_token')},retry: {maxRetries: 2,retryDelay: 500},logging: {logRequest: true,logResponse: true},},});// Returns T directly, throws on errorconstusers=awaitrequest<User[]>('/users');// Returns { data, error } — never throwsconst{ data, error }=awaitrequestSafe<User[]>('/users');Create isolated instances with different configurations. Perfect for multi-tenant apps, admin vs. public APIs, or per-domain settings.
import{createRequest}from'@gvray/request';constpublicApi=createRequest({baseURL: 'https://api.example.com'});constadminApi=createRequest({baseURL: 'https://admin.example.com',preset: {bearerAuth: {getToken: ()=>getAdminToken()},retry: {maxRetries: 3},},});constdata=awaitpublicApi<Product[]>('/products');constreport=awaitadminApi<Report>('/reports/summary');Presets are declarative configurations for built-in interceptors. They compose cleanly, register in the correct order, and eliminate repetitive setup code.
Automatically injects Authorization: Bearer <token> on every request.
importstoretifyfrom'storetify';createClient({preset: {bearerAuth: {getToken: ()=>storetify<string>('access_token'),// sync or asyncheader: 'Authorization',// defaultscheme: 'Bearer',// defaultexclude: ['/auth/login','/auth/register',/^\/public\//],},},});Checks token validity before the request is sent. If getToken() returns null or undefined, the refresh is triggered first. All concurrent requests share a single refresh — no duplicate calls.
createClient({preset: {requestAuthRefresh: {getToken: ()=>tokenStore.get('access_token'),// null = expiredrefreshToken: async()=>{constres=awaitfetch('/auth/refresh',{method: 'POST'});const{ accessToken }=awaitres.json();returnaccessToken;},setToken: (token)=>tokenStore.set('access_token',token),exclude: ['/auth/login','/auth/refresh'],},},});Triggers a token refresh when the server responds with 401 or 403. Queues all subsequent failing requests during the refresh and retries them automatically on success.
createClient({preset: {bearerAuth: {getToken: ()=>tokenStore.get('access_token')},responseAuthRefresh: {refreshToken: async()=>{/* ... */returnnewAccessToken;},setToken: (token)=>tokenStore.set('access_token',token),statuses: [401,403],// defaultloginRedirect: ()=>router.push('/login'),},},});Choosing a strategy: Use
requestAuthRefreshwhen your frontend can determine expiry (e.g. JWTexp, time-based store). UseresponseAuthRefreshwhen you rely on the server to signal expiry via status codes.
Retries failed requests with exponential backoff. Understands network errors, timeouts, and configurable status codes. Plays nicely with responseAuthRefresh — auth-retried requests are never double-retried.
createClient({preset: {retry: {maxRetries: 3,retryDelay: 500,// base delay in msexponentialBackoff: true,// 500ms → 1s → 2sretryableStatuses: [408,429,500,502,503,504],retryCondition: (error)=>error.response?.status===503,onRetry: (count,error)=>console.warn(`Retry #${count}:`,error.message),},},});Logs requests, responses, and errors with timing information. Supports custom loggers for integration with any logging infrastructure.
createClient({preset: {logging: {level: 'info',logRequest: true,logResponse: true,logError: true,logRequestBody: false,// avoid logging sensitive datalogResponseBody: false,logger: {info: (...args)=>myLogger.info(...args),error: (...args)=>myLogger.error(...args),},},},});createClient({preset: {acceptLanguage: {getLocale: ()=>i18n.language,// sync or asyncheader: 'Accept-Language',// default},},});createClient({preset: {jsonContentType: true,// auto Content-Type: application/json for non-GETwithCredentials: true,// credentials: 'include' for cross-origin},});// Auto-inferred return typeconstusers=awaitrequest<User[]>('/users');// With explicit optionsconstuser=awaitrequest<User>('/users/1',{method: 'PUT',data: {name: 'Alice'},timeout: 5000,skipAuth: true,// skip auth interceptors for this request});// Get the full response objectconstresponse=awaitrequest<User>('/users/1',{getResponse: true,// response.data, response.status, response.headers ...});// Per-request interceptors (scoped, automatically ejected after the request)constresult=awaitrequest('/upload',{method: 'POST',data: formData,requestInterceptors: [(config)=>({ ...config,onUploadProgress: (e)=>setProgress(e)})],});All preset capabilities are available as standalone interceptors for full manual control.
import{requestBearerAuth,requestAuthRefresh,createResponseAuthRefresh}from'@gvray/request';importstoretifyfrom'storetify';constmyRequest=createRequest({baseURL: '/api',requestInterceptors: [requestBearerAuth(()=>storetify<string>('access_token')),requestAuthRefresh({ getToken, refreshToken, setToken }),],});import{createCacheInterceptor}from'@gvray/request';constcache=createCacheInterceptor({ttl: 60_000,// 1 minuteonlyGet: true,exclude: ['/realtime',/\/live\//],onCacheHit: (key)=>console.log('HIT:',key),onCacheMiss: (key)=>console.log('MISS:',key),// Bring your own storage (Redis, localStorage, etc.)storage: {get: (key)=>redisClient.get(key),set: (key,value)=>redisClient.set(key,value),delete: (key)=>redisClient.del(key),clear: ()=>redisClient.flushdb(),},});constcachedRequest=createRequest({baseURL: '/api',requestInterceptors: [cache.request],responseInterceptors: [cache.response],});import{createResponseRetry}from'@gvray/request';constmyRequest=createRequest({baseURL: '/api'});// Factory form: instance is injected automaticallyconstretryInterceptor=createResponseRetry({maxRetries: 5,retryDelay: 300,exponentialBackoff: true,});import{requestTimeout}from'@gvray/request';constmyRequest=createRequest({baseURL: '/api',requestInterceptors: [requestTimeout({timeout: 3000,message: 'Request timed out'})],});import{createLoggingInterceptor}from'@gvray/request';constlogger=createLoggingInterceptor({level: 'debug',logResponseBody: true});constmyRequest=createRequest({baseURL: '/api',requestInterceptors: [logger.request],responseInterceptors: [logger.response],});Switch from Axios to the native Fetch API per-instance, no other changes required.
constfetchRequest=createRequest({engine: 'fetch',// 'axios' (default) | 'fetch'baseURL: 'https://api.example.com',preset: {bearerAuth: {getToken: ()=>token},retry: {maxRetries: 2},},});import{createClient,ErrorShowType}from'@gvray/request';createClient({errorConfig: {errorHandler: (error,opts,feedback)=>{if(error.response?.status===401){router.push('/login');return;}feedback?.({showType: ErrorShowType.ERROR_MESSAGE,message: error.message,});},errorThrower: (data)=>{// Called when response.data.success === falsethrownewError(data.errorMessage);},},});@gvray/request is written in strict TypeScript. All interceptors, configs, and response types are fully typed.
importtype{GvrayConfig,GvrayOptions,GvrayResponse,GvrayError,GvrayRequestInterceptor,GvrayResponseInterceptor,}from'@gvray/request';constmyInterceptor: GvrayRequestInterceptor=(config)=>{return{ ...config,headers: { ...config.headers,'X-App-Version': '1.0.0'}};};MIT © Gavin