v1.3
ApiCall
class ApiCall → file: core/lib/ApiCall.js
ApiCall is a high-level HTTP client built on top of HTTPClient.
It is designed to be used as a singleton in your application and is safe for concurrent use —
you can call it inside Promise.all(), Promise.race(), or any other concurrent pattern without requests aborting each other.
The reason a shared HTTPClient is not safe for concurrent use is that it holds a single XMLHttpRequest object.
Calling .open() on it while a request is in flight silently aborts the previous request.
ApiCall solves this by creating a fresh HTTPClient instance (and therefore a fresh XHR) for every request.
Instance
Instance created with new ApiCall(opts)
import { corelib } from '@mikosoft/dodo';
const apiCall = new corelib.ApiCall({
encodeURI: true,
timeout: 30000,
headers: {
'accept': 'application/json',
'content-type': 'application/json; charset=utf-8'
},
interceptor: async function () {
// 'this' is the HTTPClient instance — use it to set per-request headers
this.setReqHeader('authorization', myApp.getToken());
},
onError: (answer, err, url) => {
const msg = err?.message || answer?.statusMessage || 'API error';
console.error(`[ApiCall] ${msg} | ${url}`);
}
});
opts
| Property | Description | Type | Default |
|---|---|---|---|
| encodeURI | encode URI special characters in the URL, e.g. spaces → %20 |
boolean | false |
| timeout | request timeout in milliseconds; 0 means never timeout | number | 8000 |
| headers | default request headers sent with every request | object | {} |
| interceptor |
async function executed before every request; this is bound to the HTTPClient instance.
Use this.setReqHeader(name, value) inside to inject auth tokens or other dynamic headers.
|
Function | |
| onError |
function called on non-200 responses or thrown errors.
Signature: (answer, err, url) => void.
If omitted, errors are only logged to console.error.
|
Function |
Concurrent requests
Because each method call creates its own HTTPClient instance, you can safely run requests in parallel:
// Both run at the same time — neither aborts the other
const [balance, allowances] = await Promise.all([
apiCall.get('/api/wallet/balance'),
apiCall.get('/api/allowances')
]);
// First response wins
const result = await Promise.race([
apiCall.get('/api/primary'),
apiCall.get('/api/fallback')
]);
Methods
get(url) :Promise<any>
Send a GET request. Returns the parsed response body (theres.content field of the answer object) on HTTP 200, or undefined on error.
-
ARGUMENTS:
- url :string - the request URL
const users = await apiCall.get('https://api.example.com/users');
post(url, body) :Promise<any>
Send a POST request with a JSON body.-
ARGUMENTS:
- url :string - the request URL
- body :object - request body, serialised to JSON automatically
const result = await apiCall.post('https://api.example.com/users', { name: 'Ana', age: 30 });
put(url, body) :Promise<any>
Send a PUT request with a JSON body.-
ARGUMENTS:
- url :string - the request URL
- body :object - request body
delete(url, body) :Promise<any>
Send a DELETE request.-
ARGUMENTS:
- url :string - the request URL
- body :object - optional request body
post_form(url, formObj) :Promise<any>
Send a POST request asmultipart/form-data. The plain object is converted to FormData automatically. Useful for file uploads.
-
ARGUMENTS:
- url :string - the request URL
- formObj :object - plain object; values that are
File,FileList, or arrays are handled correctly
const result = await apiCall.post_form('/api/upload', {
title: 'My document',
file: fileInputElement.files[0]
});
async download(url, method, body) :Promise<Blob>
Fetch a file as aBlob. Sets responseType = 'blob' on the internal HTTPClient instance. The blob is isolated per call so it does not affect concurrent JSON requests.
-
ARGUMENTS:
- url :string - the request URL
- method :string - HTTP method, default
'GET' - body :any - optional request body
const blob = await apiCall.download('/api/report.pdf');
const objectUrl = URL.createObjectURL(blob);
window.open(objectUrl);
Typical application setup
Create one configured instance and import it everywhere — the singleton is safe for concurrent use.
// src/lib/apiCall.js
import { corelib } from '@mikosoft/dodo';
import $auth from '../conf/$auth.js';
import { popups } from './popups.js';
export default new corelib.ApiCall({
encodeURI: true,
timeout: 300000,
headers: {
'accept': 'application/json',
'content-type': 'application/json; charset=utf-8'
},
interceptor: async function () {
this.setReqHeader('authorization', $auth.getJWTtoken());
},
onError: (answer, err, url) => {
const msg = err?.message || answer?.res?.content?.errDoc?.message || answer?.statusMessage || 'API error';
const suffix = (answer?.status ?? err?.status) === 404 ? ` | URL: ${url}` : '';
console.error(`[ApiCall Error]: ${msg}${suffix}`);
popups.notify('ERROR', `${msg}${suffix}`, 'danger');
}
});
// In any controller
import apiCall from '/lib/apiCall.js';
async loadData() {
// Sequential
const user = await apiCall.get('/api/user/123');
// Parallel — both requests run at the same time, neither aborts the other
const [balance, allowances] = await Promise.all([
apiCall.get('/api/wallet/balance'),
apiCall.get('/api/allowances')
]);
}