npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@gvray/request

v1.0.0

Published

Universal Request Standard for Modern Web and Multi-Platform Apps

Downloads

93

Readme


@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';
import storetify from 'storetify';

createClient({
  baseURL: 'https://api.example.com',
  preset: {
    bearerAuth: {
      getToken: () => storetify<string>('access_token'),
    },
  },
});

const users = await request<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';
import storetify from '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 error
const users = await request<User[]>('/users');

// Returns { data, error } — never throws
const { data, error } = await requestSafe<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';

const publicApi = createRequest({ baseURL: 'https://api.example.com' });

const adminApi = createRequest({
  baseURL: 'https://admin.example.com',
  preset: {
    bearerAuth: { getToken: () => getAdminToken() },
    retry: { maxRetries: 3 },
  },
});

const data = await publicApi<Product[]>('/products');
const report = await adminApi<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.

import storetify from 'storetify';

createClient({
  preset: {
    bearerAuth: {
      getToken: () => storetify<string>('access_token'), // sync or async
      header: 'Authorization', // default
      scheme: 'Bearer', // default
      exclude: ['/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 = expired
      refreshToken: async () => {
        const res = await fetch('/auth/refresh', { method: 'POST' });
        const { accessToken } = await res.json();
        return accessToken;
      },
      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 () => {
        /* ... */ return newAccessToken;
      },
      setToken: (token) => tokenStore.set('access_token', token),
      statuses: [401, 403], // default
      loginRedirect: () => 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 ms
      exponentialBackoff: true, // 500ms → 1s → 2s
      retryableStatuses: [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 data
      logResponseBody: false,
      logger: {
        info: (...args) => myLogger.info(...args),
        error: (...args) => myLogger.error(...args),
      },
    },
  },
});

acceptLanguage — i18n Header Injection

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

jsonContentType and withCredentials

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

Request Options

// Auto-inferred return type
const users = await request<User[]>('/users');

// With explicit options
const user = await request<User>('/users/1', {
  method: 'PUT',
  data: { name: 'Alice' },
  timeout: 5000,
  skipAuth: true, // skip auth interceptors for this request
});

// Get the full response object
const response = await request<User>('/users/1', {
  getResponse: true,
  // response.data, response.status, response.headers ...
});

// Per-request interceptors (scoped, automatically ejected after the request)
const result = await request('/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';
import storetify from 'storetify';

const myRequest = createRequest({
  baseURL: '/api',
  requestInterceptors: [
    requestBearerAuth(() => storetify<string>('access_token')),
    requestAuthRefresh({ getToken, refreshToken, setToken }),
  ],
});

Cache

import { createCacheInterceptor } from '@gvray/request';

const cache = createCacheInterceptor({
  ttl: 60_000, // 1 minute
  onlyGet: 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(),
  },
});

const cachedRequest = createRequest({
  baseURL: '/api',
  requestInterceptors: [cache.request],
  responseInterceptors: [cache.response],
});

Retry

import { createResponseRetry } from '@gvray/request';

const myRequest = createRequest({ baseURL: '/api' });

// Factory form: instance is injected automatically
const retryInterceptor = createResponseRetry({
  maxRetries: 5,
  retryDelay: 300,
  exponentialBackoff: true,
});

Timeout

import { requestTimeout } from '@gvray/request';

const myRequest = createRequest({
  baseURL: '/api',
  requestInterceptors: [requestTimeout({ timeout: 3000, message: 'Request timed out' })],
});

Logging

import { createLoggingInterceptor } from '@gvray/request';

const logger = createLoggingInterceptor({ level: 'debug', logResponseBody: true });

const myRequest = 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.

const fetchRequest = 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 === false
      throw new Error(data.errorMessage);
    },
  },
});

TypeScript

@gvray/request is written in strict TypeScript. All interceptors, configs, and response types are fully typed.

import type {
  GvrayConfig,
  GvrayOptions,
  GvrayResponse,
  GvrayError,
  GvrayRequestInterceptor,
  GvrayResponseInterceptor,
} from '@gvray/request';

const myInterceptor: GvrayRequestInterceptor = (config) => {
  return { ...config, headers: { ...config.headers, 'X-App-Version': '1.0.0' } };
};

License

MIT © Gavin