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 (the res.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 as multipart/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 a Blob. 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')
  ]);
}