Skip to content

Repository files navigation

@gvray/request

A powerful, composable HTTP client for modern JavaScript applications.

npm versionnpm downloadsbundle sizelicenseTypeScript


@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.

Features

  • 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 any leakage
  • Composable interceptors — use presets or compose interceptors manually

Installation

npm install @gvray/request
# or
pnpm add @gvray/request
# or
yarn add @gvray/request

Quick Start

import{createClient,request}from'@gvray/request';importstoretifyfrom'storetify';createClient({baseURL: 'https://api.example.com',preset: {bearerAuth: {getToken: ()=>storetify<string>('access_token'),},},});constusers=awaitrequest<User[]>('/users');

Usage Patterns

Global Singleton

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');

Independent Instances

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');

Preset System

Presets are declarative configurations for built-in interceptors. They compose cleanly, register in the correct order, and eliminate repetitive setup code.

bearerAuth — Bearer Token Injection

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\//],},},});

requestAuthRefresh — Proactive Token Refresh

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'],},},});

responseAuthRefresh — Reactive Token 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 requestAuthRefresh when your frontend can determine expiry (e.g. JWT exp, time-based store). Use responseAuthRefresh when you rely on the server to signal expiry via status codes.

retry — Automatic Retry with Backoff

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),},},});

logging — Structured Request Logging

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),},},},});

acceptLanguage — i18n Header Injection

createClient({preset: {acceptLanguage: {getLocale: ()=>i18n.language,// sync or asyncheader: 'Accept-Language',// default},},});

jsonContentType and withCredentials

createClient({preset: {jsonContentType: true,// auto Content-Type: application/json for non-GETwithCredentials: true,// credentials: 'include' for cross-origin},});

Request Options

// 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)})],});

Standalone Interceptors

All preset capabilities are available as standalone interceptors for full manual control.

Auth

import{requestBearerAuth,requestAuthRefresh,createResponseAuthRefresh}from'@gvray/request';importstoretifyfrom'storetify';constmyRequest=createRequest({baseURL: '/api',requestInterceptors: [requestBearerAuth(()=>storetify<string>('access_token')),requestAuthRefresh({ getToken, refreshToken, setToken }),],});

Cache

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],});

Retry

import{createResponseRetry}from'@gvray/request';constmyRequest=createRequest({baseURL: '/api'});// Factory form: instance is injected automaticallyconstretryInterceptor=createResponseRetry({maxRetries: 5,retryDelay: 300,exponentialBackoff: true,});

Timeout

import{requestTimeout}from'@gvray/request';constmyRequest=createRequest({baseURL: '/api',requestInterceptors: [requestTimeout({timeout: 3000,message: 'Request timed out'})],});

Logging

import{createLoggingInterceptor}from'@gvray/request';constlogger=createLoggingInterceptor({level: 'debug',logResponseBody: true});constmyRequest=createRequest({baseURL: '/api',requestInterceptors: [logger.request],responseInterceptors: [logger.response],});

Engine Switching

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},},});

Error Handling

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);},},});

TypeScript

@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'}};};

License

MIT © Gavin

About

Universal Request Standard for Modern Web and Multi-Platform Apps

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages