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

tanvir

v1.0.2

Published

Tanvir Dalal — a production-grade, extensible HTTP client with fluent API, interceptors, retries, and cancellation.

Downloads

28

Readme

Tanvir Dalal

A modern, production-grade HTTP client for JavaScript.

Zero dependencies · Pure ESM · Built on native fetch

npm license node


import tavirDalal from 'tanvir';

const { data } = await tavirDalal.get('https://api.example.com/users');

That's it. No setup. No boilerplate.


Why Tanvir Dalal?

The native fetch API is powerful — but raw. Every real project ends up wrapping it with the same patterns: base URLs, auth headers, error handling, retries, cancellation. Tanvir Dalal gives you all of that, pre-built and well-tested.

| Without Tanvir Dalal | With Tanvir Dalal | |---|---| | Manually build URLs each time | Set baseURL once on an instance | | Try/catch every request | Structured TanvirDalalError with error codes | | Write retry logic yourself | retries: 3 in config | | Abort handling boilerplate | Built-in AbortController + TanvirDalalCancelToken | | Repeated header logic | Request interceptors | | response.json() every time | Auto-parsed, ready in res.data |


Table of Contents


Installation

npm install tanvir

Requirements: Node.js 18 or later. Tanvir Dalal uses native fetch and AbortSignal — no polyfills needed.


Quick Start

Your first request

import tavirDalal from 'tanvir';

// async/await
const res = await tavirDalal.get('https://jsonplaceholder.typicode.com/posts/1');
console.log(res.data); // { id: 1, title: '...', ... }

Named import

import { tavirDalal } from 'tanvir';

const res = await tavirDalal.get('/users');

All HTTP methods

// GET
const res = await tavirDalal.get('/users');

// POST — pass data as the second argument
const res = await tavirDalal.post('/users', { name: 'Tanvir', role: 'admin' });

// PUT / PATCH
await tavirDalal.put('/users/1', { name: 'Updated Name' });
await tavirDalal.patch('/users/1', { role: 'viewer' });

// DELETE
await tavirDalal.delete('/users/1');

Fluent .get(cb) style

Tanvir Dalal also supports a fluent callback syntax — great for quick scripts:

tavirDalal
  .get('/posts')
  .get(res => console.log(res.data))
  .catch(err => console.error(err.message));

Tip: Both styles (async/await and fluent) work everywhere. Pick whichever fits your code.


Common Use Cases

These are the patterns you'll use in almost every real project.

Fetching user data

const res = await tavirDalal.get('https://api.myapp.com/users/42');
console.log(res.data.name);

Logging in

const res = await tavirDalal.post('https://api.myapp.com/auth/login', {
  email: '[email protected]',
  password: 'hunter2',
});

const token = res.data.token;

Sending auth headers on every request

Create a named instance for your API and attach auth once via an interceptor:

const api = tavirDalal.create({ baseURL: 'https://api.myapp.com' });

api.interceptors.request.use((config) => {
  const token = localStorage.getItem('token');
  if (token) config.headers['Authorization'] = `Bearer ${token}`;
  return config;
});

// Every request through `api` now sends the token automatically
const res = await api.get('/dashboard');

Handling errors gracefully

try {
  const res = await tavirDalal.get('/protected-route');
} catch (err) {
  if (tavirDalal.isDalalError(err)) {
    switch (err.code) {
      case 'ERR_BAD_RESPONSE':
        console.error('Server error:', err.response.status);
        break;
      case 'ERR_NETWORK':
        console.error('No internet connection');
        break;
      case 'ERR_TIMEOUT':
        console.error('Request took too long');
        break;
    }
  }
}

Query params

// Produces: /posts?userId=1&_limit=5
const res = await tavirDalal.get('/posts', {
  params: { userId: 1, _limit: 5 },
});

// Arrays work too: /posts?tags=js&tags=node
const res = await tavirDalal.get('/posts', {
  params: { tags: ['js', 'node'] },
});

Cancelling a request in React

useEffect(() => {
  const controller = new AbortController();

  tavirDalal.get('/data', { signal: controller.signal })
    .then(res => setData(res.data))
    .catch(err => {
      if (!tavirDalal.isCancel(err)) setError(err.message);
    });

  return () => controller.abort(); // cancel when component unmounts
}, []);

Core Concepts

The Response Object

Every Tanvir Dalal request resolves with a response envelope — an object that gives you the data and full context:

const res = await tavirDalal.get('/users/1');

res.data       // → parsed response body (auto-JSON-parsed)
res.status     // → 200
res.statusText // → 'OK'
res.headers    // → { 'content-type': 'application/json', ... }
res.config     // → the full config used for this request

Common mistake: Forgetting that res is the envelope, not the data. Always use res.data to access your payload.


Instances

For most apps, you'll want a named instance tied to your API. This lets you set a baseURL, default headers, and timeout once — and never repeat them.

// api.js — create once, import everywhere
import { tavirDalal } from 'tanvir';

const api = tavirDalal.create({
  baseURL: 'https://api.myapp.com/v2',
  timeout: 10000,                         // 10 second timeout
  headers: {
    common: { 'x-app-version': '1.0.2' }, // sent with every request
  },
});

export default api;
// users.js — use the instance
import api from './api.js';

export const getUser    = (id)   => api.get(`/users/${id}`);
export const createUser = (data) => api.post('/users', data);

Each create() call returns an independent instance — interceptors, defaults, and config never leak between instances.

How config is merged

Config is resolved in three layers. Later layers override earlier ones:

Global defaults  →  Instance config  →  Per-request config
   (lowest)                                  (highest)
  • Headers — deep merged per HTTP method (common, get, post, …)
  • Params — shallow merged (request params override instance params)
  • Everything else — later value wins

Config & Defaults

Here is every config option Tanvir Dalal supports, with inline explanations:

const res = await tavirDalal.get(url, {

  // Base URL — prepended to all relative request URLs
  baseURL: 'https://api.example.com',

  // Query params — appended to the URL automatically
  params: { page: 1, tags: ['js', 'node'] },

  // Custom param serialiser — overrides the built-in one
  paramsSerializer: (params) => new URLSearchParams(params).toString(),

  // Headers — supports per-method namespacing
  headers: {
    common: { 'x-request-id': '123' }, // all methods
    get:    { 'x-cache': 'true' },     // GET only
    post:   { 'x-idempotency-key': uuid() }, // POST only
  },

  // Status validation — return false to treat that status as an error
  // Default: status >= 200 && status < 300
  validateStatus: (status) => status < 500,

  // Timeout in milliseconds — 0 disables it
  timeout: 8000,

  // Retry config
  retries: 3,
  retryDelay: 300,                         // base wait time in ms
  retryBackoff: 'exponential',             // 'exponential' | 'linear' | 'fixed'
  retryOnStatusCodes: [429, 500, 502, 503, 504],
  retryOnNetworkError: true,

  // Cancellation
  signal: controller.signal,              // from an AbortController
  cancelToken: source.token,             // from TanvirDalalCancelToken.source()

  // Transform pipelines (see Transform Hooks section)
  transformRequest:  [(data, headers) => data],
  transformResponse: [(data) => data],

});

Interceptors

Interceptors let you run code on every request or response — without touching individual call sites. This is the right place for auth tokens, logging, error normalisation, and more.

Adding a request interceptor

// Returns an ID you can use to remove this interceptor later
const id = api.interceptors.request.use(
  (config) => {
    // Modify the config — then return it
    config.headers['Authorization'] = `Bearer ${getToken()}`;
    return config;
  },
  (error) => {
    // Handle an error before the request fires
    return Promise.reject(error);
  }
);

Adding a response interceptor

const id = api.interceptors.response.use(
  (response) => {
    // Transform the response before it reaches your code
    return response.data; // unwrap — callers get data directly
  },
  (error) => {
    // Handle HTTP errors centrally
    if (error.response?.status === 401) {
      return refreshTokenAndRetry(error.config);
    }
    return Promise.reject(error);
  }
);

Removing an interceptor

api.interceptors.request.eject(id);
api.interceptors.response.eject(id);

Execution order

| Type | Order | |---|---| | Request interceptors | First registered → first to run | | Response interceptors | First registered → first to receive response |

Best practice: Add interceptors once when creating your instance, not inside component render loops or request functions.


Advanced Features

Cancellation

Use cancellation when users navigate away, components unmount, or a newer request supersedes an older one.

With AbortController (recommended)

const controller = new AbortController();

const res = await tavirDalal.get('/search', {
  params: { q: 'tanvir' },
  signal: controller.signal,
});

// Cancel it from anywhere
controller.abort();

With TanvirDalalCancelToken (Axios-compatible style)

import { TanvirDalalCancelToken } from 'tanvir';

const source = TanvirDalalCancelToken.source();

tavirDalal.get('/data', { cancelToken: source.token });

// Pass a reason
source.cancel('User navigated away');

Detecting a cancellation

try {
  await tavirDalal.get('/data', { signal });
} catch (err) {
  if (tavirDalal.isCancel(err)) {
    console.log('Request was cancelled:', err.message);
  }
}

Retry

Tanvir Dalal retries failed requests automatically when configured. Retries are transparent — your interceptors still fire on each attempt.

const api = tavirDalal.create({
  retries: 3,               // try up to 3 times after the first failure
  retryDelay: 300,          // base wait time in ms
  retryBackoff: 'exponential',

  // Which status codes should trigger a retry?
  retryOnStatusCodes: [408, 429, 500, 502, 503, 504],

  // Also retry on network errors (DNS, offline, etc.)
  retryOnNetworkError: true,
});

Backoff strategies

| Strategy | Formula | Example (base = 300ms) | |---|---|---| | exponential | base × 2ⁿ + jitter | 300ms → 600ms + jitter → 1200ms + jitter | | linear | base × attempt | 300ms → 600ms → 900ms | | fixed | base (always) | 300ms → 300ms → 300ms |

Tip: Use exponential for transient server errors (500, 503). It reduces thundering-herd pressure on a struggling backend.


Transform Hooks

Transforms let you pre-process request data before it's sent and post-process response data before it reaches your code. Each transform receives the output of the previous one — they form a pipeline.

Unwrapping a common API envelope

Many APIs wrap their responses: { success: true, payload: { ... } }. Use a response transform to unwrap it globally:

const api = tavirDalal.create({
  transformResponse: [
    // Step 1 (default): parse JSON string → object
    (data) => { try { return JSON.parse(data); } catch { return data; } },

    // Step 2: unwrap the envelope
    (data) => data?.payload ?? data,
  ],
});

// Now callers get the inner payload directly
const res = await api.get('/users/1');
console.log(res.data); // { id: 1, name: 'Tanvir' } — not { success, payload }

Adding metadata to every outgoing request

const api = tavirDalal.create({
  transformRequest: [
    // Default: auto-stringify objects to JSON
    (data) => typeof data === 'object' ? JSON.stringify(data) : data,

    // Wrap in an envelope with a timestamp
    (data) => JSON.stringify({ payload: JSON.parse(data), sentAt: Date.now() }),
  ],
});

Error Handling

Every error Tanvir Dalal throws is a TanvirDalalError — a structured object with a consistent shape. This makes it easy to write a single error handler that covers all failure modes.

The error shape

try {
  await api.get('/admin');
} catch (err) {
  if (tavirDalal.isDalalError(err)) {
    err.name         // → 'DalalError'
    err.code         // → 'ERR_BAD_RESPONSE'  (see table below)
    err.message      // → 'Request failed with status 403'
    err.config       // → the full config used for this request
    err.response     // → { data, status, statusText, headers }
    err.isDalalError // → true
  }
}

Error codes

| Code | When it happens | |---|---| | ERR_BAD_RESPONSE | The server responded, but the status code failed validateStatus | | ERR_NETWORK | No response received — DNS failure, CORS, offline | | ERR_TIMEOUT | Request exceeded the timeout you configured | | ERR_CANCELLED | Request was aborted via AbortController or TanvirDalalCancelToken |

Centralised error handling with interceptors

Instead of handling errors in every catch block, handle them once:

api.interceptors.response.use(
  (response) => response,
  (error) => {
    if (!tavirDalal.isDalalError(error)) throw error;

    switch (error.code) {
      case 'ERR_BAD_RESPONSE':
        if (error.response.status === 401) redirectToLogin();
        if (error.response.status === 429) showRateLimitWarning();
        break;
      case 'ERR_NETWORK':
        showOfflineBanner();
        break;
      case 'ERR_TIMEOUT':
        showTimeoutToast();
        break;
    }

    return Promise.reject(error); // re-throw so callers can still catch it
  }
);

API Reference

HTTP Methods

All methods return a TanvirDalalPromise — a native Promise subclass. It works with await, .then(), .catch(), Promise.all(), and adds .get(cb) as a fluent alias for .then().

| Method | Signature | |---|---| | tavirDalal.get | (url, config?) → TanvirDalalPromise | | tavirDalal.post | (url, data?, config?) → TanvirDalalPromise | | tavirDalal.put | (url, data?, config?) → TanvirDalalPromise | | tavirDalal.patch | (url, data?, config?) → TanvirDalalPromise | | tavirDalal.delete | (url, config?) → TanvirDalalPromise | | tavirDalal.head | (url, config?) → TanvirDalalPromise | | tavirDalal.options | (url, config?) → TanvirDalalPromise | | tavirDalal.request | (config) → TanvirDalalPromise |

All resolve with:

{
  data:       any,      // parsed response body
  status:     number,   // e.g. 200
  statusText: string,   // e.g. 'OK'
  headers:    object,   // lowercased header keys
  config:     object,   // the merged config used
  request:    Response, // the raw fetch Response
}

Instance

// Create a new, independent instance
const api = tavirDalal.create(config);

// Read the current defaults for this instance
api.defaults; // → merged snapshot of global + instance config

Interceptors

// Register — returns a numeric ID
const id = instance.interceptors.request.use(fulfilled, rejected?);
const id = instance.interceptors.response.use(fulfilled, rejected?);

// Remove
instance.interceptors.request.eject(id);
instance.interceptors.response.eject(id);

Utilities

tavirDalal.isDalalError(value) // → true if value is a TanvirDalalError
tavirDalal.isCancel(value)     // → true if request was cancelled

Named Exports

import tavirDalal, {
  tavirDalal,                    // named — same as default
  TanvirDalalError,              // the error class
  TanvirDalalPromise,            // the fluent promise class
  TanvirDalalCancelToken,        // cancellation primitive
  TanvirDalalInterceptorManager, // the interceptor queue class
  Dalal,                         // internal class (advanced / subclassing)
} from 'tanvir';

Internal Architecture

This section is for those who want to understand how Tanvir Dalal works under the hood, or who want to contribute.

Folder structure

tanvir/
├── index.js                      ← root re-export (npm entry point)
├── example.js                    ← basic usage  →  node example.js
├── examples/
│   └── advanced.js               ← advanced patterns  →  node examples/advanced.js
└── src/
    ├── index.js                  ← public API + all named exports
    ├── core/
    │   ├── Dalal.js              ← main class, interceptor chain builder
    │   ├── DalalError.js         ← structured error with factory methods
    │   ├── DalalPromise.js       ← .get(cb) fluent promise subclass
    │   ├── InterceptorManager.js ← O(1) eject, stable numeric IDs
    │   ├── CancelToken.js        ← bridges to AbortController internally
    │   ├── dispatchRequest.js    ← fetch wrapper + retry + timeout
    │   └── defaults.js           ← deeply frozen global defaults
    └── utils/
        ├── mergeConfig.js        ← 3-layer config merger
        └── params.js             ← query string serialiser + URL builder

How a request flows

tavirDalal.get(url, config)
       │
       ▼
  mergeConfig()              ← resolves: defaults → instance → request
       │
       ▼
  Request Interceptors       ← FIFO, each receives and returns config
       │
       ▼
  dispatchRequest()
    ├── buildURL()           ← appends serialised params
    ├── transformRequest[]   ← pipeline applied to request body
    ├── fetch() + timeout    ← native transport
    ├── retry engine         ← re-runs on eligible failures
    ├── transformResponse[]  ← pipeline applied to response body
    └── validateStatus()     ← throws TanvirDalalError if status fails
       │
       ▼
  Response Interceptors      ← push order, each receives response envelope
       │
       ▼
  TanvirDalalPromise         ← returned to caller, supports .get(cb)

License

MIT © sisrar