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

@vorthain/vortex

v0.2.8

Published

A modern, highly-configurable API client designed for DX. Works in both browser and Node.js environments.

Readme

🌀 @vorthain/vortex

npm Downloads Bundle Size

Configuration-first HTTP client with developer experience in mind

// Configure your entire API surface
const client = createVortexClient({
  baseURL: 'https://api.example.com',
  endpoints: {
    users: { path: '/users' },
    user: { path: '/users/:id' },
    posts: { path: '/posts' },
  },
});

// Clean, predictable API
const users = await client.get('users').send();
const user = await client.get('user').pathParams({ id: 123 }).send();

Installation

npm install @vorthain/vortex

Why Vortex?

Traditional HTTP clients make you repeat yourself constantly - auth headers, error handling, retry logic, response transformation. You write the same patterns in every project, in every file, sometimes in every request.

Vortex flips this: configure your patterns once, use them everywhere.

// Define once
const api = createVortexClient({
  baseURL: 'https://api.example.com',
  timeout: 5000,

  // Auto-attach auth to every request
  requestInterceptor: async (config) => {
    config.headers.Authorization = `Bearer ${await getToken()}`;
    return config;
  },

  // Handle token refresh automatically
  errorInterceptor: async (error, config, retry) => {
    if (error.status === 401 && !config._tokenRefreshed) {
      const newToken = await refreshToken();
      return retry({
        headers: { Authorization: `Bearer ${newToken}` },
        _tokenRefreshed: true,
      });
    }
    throw error;
  },

  endpoints: {
    users: { path: '/users' },
    user: { path: '/users/:id' },
    posts: { path: '/posts' },
  },
});

// Use everywhere - auth, retries, errors all handled
const user = await api.get('user').pathParams({ id: 123 }).send();

Core Features

Configuration Hierarchy

Settings cascade from global → endpoint → method → request level:

const api = createVortexClient({
  timeout: 30000, // Global default

  endpoints: {
    users: {
      path: '/users',
      timeout: 10000, // Override for all /users requests

      methods: {
        get: {
          cache: { enabled: true }, // Only GET /users cached
        },
        post: {
          timeout: 5000, // POST /users has 5s timeout
        },
      },
    },
  },
});

// Request-level override (highest priority)
await api.get('users').settings({ timeout: 2000 }).send();

Interceptors

Interceptors let you modify requests/responses or handle errors globally:

const api = createVortexClient({
  baseURL: 'https://api.example.com',

  // Modify all requests before sending
  requestInterceptor: async (config) => {
    // config has: url, method, headers, body, timeout, responseType, pathParams, searchParams
    config.headers.Authorization = `Bearer ${await getToken()}`;
    config.headers['X-Request-ID'] = generateUUID();

    // Add timestamp for response interceptor to use
    config._startTime = Date.now();

    // Must return the config
    return config;
  },

  // Modify all responses after receiving
  responseInterceptor: async (response, config) => {
    // Log response time using timestamp from requestInterceptor
    if (config._startTime) {
      console.log(`Request to ${config.url} took ${Date.now() - config._startTime}ms`);
    }

    // Check for deprecation warnings
    const warning = response.headers.get('X-Deprecation-Warning');
    if (warning) {
      console.warn(`API Deprecation: ${warning}`);
    }

    // Must return the response
    return response;
  },

  // Handle all errors with retry capability
  errorInterceptor: async (error, config, retry) => {
    // Token refresh on 401
    if (error.status === 401 && !config._tokenRefreshed) {
      const newToken = await refreshAuthToken();
      return retry({
        headers: { Authorization: `Bearer ${newToken}` },
        _tokenRefreshed: true,
      });
    }

    // Retry on 503 with delay from response headers
    if (error.status === 503) {
      // Server can send Retry-After header which we capture in metadata
      const retryAfter = error.metadata?.headers?.['retry-after'];
      const delay = retryAfter ? parseInt(retryAfter) * 1000 : 5000;
      await new Promise((resolve) => setTimeout(resolve, delay));

      const result = await retry();
      if (result !== undefined) {
        return result;
      }
    }

    throw error;
  },

  endpoints: {
    users: { path: '/users' },
  },
});

Smart Retry with Interceptors

const api = createVortexClient({
  baseURL: 'https://api.example.com',
  maxRetries: 3, // Built-in retry limit (applies to all retry() calls)

  errorInterceptor: async (error, config, retry) => {
    // Token refresh
    if (error.status === 401 && !config._tokenRefreshed) {
      const newToken = await refreshAuthToken();
      return retry({
        headers: { Authorization: `Bearer ${newToken}` },
        _tokenRefreshed: true,
      });
    }

    // Exponential backoff for server errors
    if (error.status >= 500) {
      const attempt = config._retryCount || 0;
      const delay = Math.min(1000 * Math.pow(2, attempt), 10000);
      await new Promise((resolve) => setTimeout(resolve, delay));

      const result = await retry();
      // retry() returns undefined when maxRetries is reached
      if (result !== undefined) {
        return result;
      }
    }

    throw error;
  },

  endpoints: {
    users: { path: '/users' },
  },
});

Caching

Vortex includes a built-in in-memory cache with TTL support:

const api = createVortexClient({
  baseURL: 'https://api.example.com',

  // Enable simple caching globally
  cache: {
    enabled: true,
    ttl: 60000, // Cache for 1 minute
    strategy: 'simple', // Standard cache
  },

  endpoints: {
    users: {
      path: '/users',
      methods: {
        get: {
          cache: {
            enabled: true,
            ttl: 300000, // Override: cache users for 5 minutes
          },
        },
      },
    },
  },
});

// First call hits server
const users1 = await api.get('users').send();

// Second call returns from cache (within TTL)
const users2 = await api.get('users').send();

// Force fresh data
const fresh = await api
  .get('users')
  .settings({ cache: { enabled: false } })
  .send();

// Clear all cache
await api.clearCache();

Stale-While-Revalidate (SWR)

SWR returns cached data immediately while fetching fresh data in the background:

const api = createVortexClient({
  baseURL: 'https://api.example.com',

  cache: {
    enabled: true,
    strategy: 'swr', // Return stale, revalidate in background
    ttl: 30000,
  },

  endpoints: {
    dashboard: {
      path: '/dashboard',
      methods: {
        get: {
          onRevalidate: (freshData) => {
            // Called when fresh data arrives
            updateUI(freshData);
          },
        },
      },
    },
  },
});

// Instant response from cache, fresh data incoming
const data = await api.get('dashboard').send();

Custom Cache Implementation

You can provide your own cache implementation:

class RedisCache {
  async get(key) {
    /* ... */
  }
  async set(key, value, ttl) {
    /* ... */
  }
  async delete(key) {
    /* ... */
  }
  async has(key) {
    /* ... */
  }
  async clear() {
    /* ... */
  }
}

const api = createVortexClient({
  cache: {
    enabled: true,
    instance: new RedisCache(),
    ttl: 60000,
  },
  // ... rest of config
});

Request Building

// Path parameters
await api.get('user').pathParams({ id: 123 }).send();
// → GET /users/123

// Query parameters
await api.get('users').search({ role: 'admin', active: true }).send();
// → GET /users?role=admin&active=true

// Request body
await api.post('users').body({ name: 'John', email: '[email protected]' }).send();

// Custom settings
await api
  .get('users')
  .settings({
    timeout: 5000,
    headers: { 'X-Priority': 'high' },
    onSuccess: (data) => console.log('Got users:', data),
    onError: (error) => console.error('Failed:', error),
  })
  .send();

// Cancel request
const request = api.get('large-dataset');
const promise = request.send();

setTimeout(() => request.cancel('Taking too long'), 5000);

Special HTTP Methods

// OPTIONS - Check CORS and allowed methods
let allowedMethods = '';
await api
  .options('users')
  .settings({
    responseInterceptor: (response, config) => {
      allowedMethods = response.headers.get('Allow');
      console.log('Allowed methods:', allowedMethods);
      console.log('CORS:', response.headers.get('Access-Control-Allow-Origin'));
      return response; // Must return the response
    },
  })
  .send()
  .catch((error) => console.log('OPTIONS failed:', error.status));
// Returns null for OPTIONS, but headers were captured

// HEAD - Check if resource exists and get metadata
let lastModified;
try {
  await api
    .head('document')
    .pathParams({ id: 'abc' })
    .settings({
      responseInterceptor: (response, config) => {
        lastModified = response.headers.get('Last-Modified');
        console.log('Document size:', response.headers.get('Content-Length'));
        return response;
      },
    })
    .send();
  console.log('Document exists, last modified:', lastModified);
} catch (error) {
  if (error.status === 404) console.log('Document not found');
}

Response Handling

You can handle responses in multiple ways:

// 1. Async/await with try-catch
try {
  const users = await api.get('users').send();
  console.log('Success:', users);
} catch (error) {
  if (error.hasStatus(404)) {
    console.log('Not found');
  } else {
    console.error('Error:', error.message);
  }
}

// 2. Promise chains
api
  .get('users')
  .send()
  .then((users) => {
    console.log('Success:', users);
    return users.filter((u) => u.active);
  })
  .then((activeUsers) => {
    console.log('Active users:', activeUsers);
  })
  .catch((error) => {
    console.error('Failed:', error);
  })
  .finally(() => {
    console.log('Request completed');
  });

// 3. Callbacks in settings
await api
  .get('users')
  .settings({
    onStart: () => {
      console.log('Request starting...');
      showLoader();
    },
    onSuccess: (users) => {
      console.log('Got users:', users);
      updateUI(users);
    },
    onError: (error) => {
      console.error('Failed:', error);
      showErrorMessage(error.message);
    },
    onFinally: () => {
      console.log('Request done');
      hideLoader();
    },
  })
  .send();

// 4. Mix approaches - callbacks for side effects, promise for main flow
try {
  const users = await api
    .get('users')
    .settings({
      onStart: () => showLoader(),
      onFinally: () => hideLoader(), // Always runs, even if error is thrown
    })
    .send();

  // Main business logic here
  processUsers(users);
} catch (error) {
  // hideLoader() has already been called by onFinally
  handleError(error);
}

Progress Tracking (Browser only)

await api
  .post('upload')
  .body(formData)
  .settings({
    onUploadProgress: (progress) => {
      console.log(`Upload: ${Math.round(progress.progress * 100)}%`);
    },
    onDownloadProgress: (progress) => {
      console.log(`Download: ${Math.round(progress.progress * 100)}%`);
    },
  })
  .send();

Response & Error Transformation

Transform data at any level of the configuration hierarchy:

const api = createVortexClient({
  baseURL: 'https://api.example.com',

  // Transform all successful responses
  responseMapper: (data) => ({
    ...data,
    _retrieved: Date.now(),
  }),

  // Transform all errors
  errorMapper: (error) => {
    // Add context for debugging
    error.timestamp = Date.now();
    error.userId = getCurrentUserId();

    // Send to monitoring service
    if (typeof window !== 'undefined' && window.trackError) {
      window.trackError(error);
    }

    return error;
  },

  endpoints: {
    users: {
      path: '/users',

      // Override for this endpoint
      responseMapper: (data) => data.users || data,

      methods: {
        post: {
          // Don't inherit mappers from parent levels
          inheritMappers: false,

          // Custom mapper just for POST /users
          responseMapper: (response) => ({
            success: true,
            user: response,
          }),
        },
      },
    },
  },
});

Custom Status Validation

Control which HTTP status codes are considered successful:

const api = createVortexClient({
  baseURL: 'https://api.example.com',

  // Global: only 2xx is success (default)
  validateStatus: (status) => status >= 200 && status < 300,

  endpoints: {
    auth: {
      path: '/auth',
      methods: {
        post: {
          // Accept 201 and 202 for auth
          validateStatus: (status) => status === 201 || status === 202,
        },
      },
    },
    legacy: {
      path: '/legacy-api',
      // This old API returns 400 for "no results" - treat as success
      validateStatus: (status) => status === 200 || status === 400,
    },
  },
});

// Per-request override
await api
  .get('users')
  .settings({
    validateStatus: (status) => status < 500, // 4xx and below are "success"
  })
  .send();

Response Types

By default, all responses are parsed as JSON. You only need to specify responseType for non-JSON content:

// JSON is default - no need to specify
const users = await api.get('users').send();
const result = await api.post('data').body({...}).send();

// Specify only for non-JSON responses
const html = await api.get('page')
  .settings({ responseType: 'text' })
  .send();

const image = await api.get('avatar')
  .settings({ responseType: 'blob' })
  .send();
// Use: URL.createObjectURL(image)

const buffer = await api.get('file')
  .settings({ responseType: 'arrayBuffer' })
  .send();

// FormData (rare for responses)
const formData = await api.post('form')
  .settings({ responseType: 'formData' })
  .send();

// Set different defaults per endpoint
const api = createVortexClient({
  endpoints: {
    images: {
      path: '/images/:id',
      responseType: 'blob' // All image endpoints return blobs
    }
  }
});

Redirect Handling

Control how HTTP redirects are handled:

const api = createVortexClient({
  baseURL: 'https://api.example.com',

  // Global default
  redirect: 'follow', // Auto-follow redirects (default)

  endpoints: {
    auth: {
      path: '/auth/login',
      methods: {
        post: {
          // Don't follow redirects - get the 3xx response
          redirect: 'manual',
          // Must also accept 3xx as valid status
          validateStatus: (status) => status >= 200 && status < 400,
        },
      },
    },
    strict: {
      path: '/strict-api',
      // Throw error on redirects
      redirect: 'error',
    },
  },
});

// Handle redirect manually
const response = await api.post('auth').body({ username, password }).send();

if (response.status === 302) {
  const redirectUrl = response.headers.get('location');
  window.location.href = redirectUrl;
}

Creating Client Variants

Use withConfig() to create modified versions of a client:

const baseApi = createVortexClient({
  baseURL: 'https://api.example.com',
  timeout: 30000,
  headers: {
    'X-API-Version': '1.0',
  },
  endpoints: {
    users: { path: '/users' },
    posts: { path: '/posts' },
  },
});

// Create authenticated variant
const authApi = baseApi.withConfig({
  headers: {
    Authorization: `Bearer ${token}`,
  },
});

// Create variant with different timeout
const fastApi = baseApi.withConfig({
  timeout: 5000,
});

// Create variant for different environment
const stagingApi = baseApi.withConfig({
  baseURL: 'https://staging-api.example.com',
});

// Original client unchanged
await baseApi.get('users').send(); // No auth header
await authApi.get('users').send(); // Has auth header

Cache Statistics & Management

Monitor and manage your cache:

const api = createVortexClient({
  baseURL: 'https://api.example.com',
  cache: {
    enabled: true,
    ttl: 60000,
  },
  endpoints: {
    users: { path: '/users' },
  },
});

// Make some requests
await api.get('users').send();
await api.get('users').send(); // From cache

// Get cache statistics
const stats = await api.getCacheStats();
console.log(stats);
// {
//   hits: 1,
//   misses: 1,
//   sets: 1,
//   size: 1,
//   maxSize: 1000,
//   hitRate: 0.5
// }

// Clear specific endpoints or all
await api.clearCache();

// Clean up when done (important for Node.js apps)
await api.destroy();

Cleanup & Resource Management

Properly clean up resources, especially in Node.js applications:

const api = createVortexClient({
  baseURL: 'https://api.example.com',
  cache: { enabled: true },
  endpoints: {
    users: { path: '/users' },
  },
});

try {
  // Use the client
  await api.get('users').send();
} finally {
  // Clean up timers, cache, and pending requests
  await api.destroy();
}

// In a Node.js server
process.on('SIGTERM', async () => {
  await api.destroy();
  process.exit(0);
});

Error Handling

Every error is a VortexError with rich context:

try {
  await api.get('user').pathParams({ id: 999 }).send();
} catch (error) {
  console.log(error.type); // 'HTTP_ERROR'
  console.log(error.status); // 404
  console.log(error.responseBody); // Server's error response
  console.log(error.requestConfig); // Full request configuration

  // Utility methods
  if (error.isType(VortexError.TYPES.HTTP)) {
    /* ... */
  }
  if (error.hasStatus(404)) {
    /* ... */
  }
  if (error.isClientError()) {
    /* 4xx */
  }
  if (error.isServerError()) {
    /* 5xx */
  }
}

Error types:

  • HTTP_ERROR - 4xx/5xx responses
  • NETWORK_ERROR - Connection failures
  • TIMEOUT_ERROR - Request timeout
  • ABORT_ERROR - Cancelled requests
  • PARSE_ERROR - Response parsing failures
  • VALIDATION_ERROR - Invalid configuration
  • CONFIG_ERROR - Setup errors

Parallel Requests

// Wait for all
const [users, posts] = await Promise.all([api.get('users').send(), api.get('posts').send()]);

// Handle failures individually
const results = await Promise.allSettled([api.get('users').send(), api.get('posts').send()]);

results.forEach((result, index) => {
  if (result.status === 'fulfilled') {
    console.log(`Request ${index} succeeded:`, result.value);
  } else {
    console.log(`Request ${index} failed:`, result.reason);
  }
});

API Reference

createVortexClient(config)

Creates a new client instance.

client.get/post/put/patch/delete/head/options(endpoint)

Create a request builder for the specified endpoint.

client.request(method, endpoint)

Create a request with custom HTTP method.

Request Builder Methods

  • .pathParams(params) - Replace path parameters
  • .search(params) - Add query parameters
  • .body(data) - Set request body
  • .settings(config) - Override request settings
  • .cancel(message?) - Cancel the request
  • .send() - Execute and return promise

Utility Methods

  • client.withConfig(config) - Create new client with merged config
  • client.clearCache() - Clear all cached responses
  • client.getCacheStats() - Get cache statistics
  • client.destroy() - Clean up resources