tanvir
v1.0.2
Published
Tanvir Dalal — a production-grade, extensible HTTP client with fluent API, interceptors, retries, and cancellation.
Downloads
28
Maintainers
Readme
Tanvir Dalal
A modern, production-grade HTTP client for JavaScript.
Zero dependencies · Pure ESM · Built on native fetch
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
- Quick Start
- Common Use Cases
- Core Concepts
- Interceptors
- Advanced Features
- Error Handling
- API Reference
- Internal Architecture
Installation
npm install tanvirRequirements: Node.js 18 or later. Tanvir Dalal uses native
fetchandAbortSignal— 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 requestCommon mistake: Forgetting that
resis the envelope, not the data. Always useres.datato 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
exponentialfor 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 configInterceptors
// 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 cancelledNamed 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 builderHow 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
