npm.io
2.2.0 • Published 2 weeks ago

easy-currencies

Licence
ISC
Version
2.2.0
Deps
0
Size
157 kB
Vulns
0
Weekly
0
Stars
14

easy-currencies

npm version CI

Convert currencies with ease! Eight exchange rate providers to choose from, others easily implementable.

Features

  • Easily convert currencies using one of the eight built-in API providers
  • Two modes of operation:
    • Easy mode - no configuration or API keys required at all
    • Custom mode - choose one or more providers, use key-gated providers.
  • Add custom providers (private or public)
  • Provider fallbacks - automatic switching of active providers in the case of failure

Install

$ npm install easy-currencies

Usage (Easy/chain mode)

Easy/chain mode does not require initialization, and thus uses the default, no API key-required provider (api.exchangerate-api.com)

// CommonJS
const { Convert } = require("easy-currencies");
// ES6
import { Convert } from "easy-currencies";

const value = await Convert(15).from("USD").to("EUR");

console.log(value); // converted value

Usage (custom mode)

Default provider initialization, no key needed

import { Converter } from "easy-currencies";

const converter = new Converter();
const value = await converter.convert(15, "USD", "EUR");

console.log(value); // converted value

Usage (raw mode / cached mode)

Use this to get a JSON of conversion rates from your current provider.

import { Convert } from "easy-currencies";

const convert = await Convert().from("USD").fetch();

console.log(convert.rates);
// {
//   USD: 1,
//   EUR: 0.858,
//   GBP: 0.736,
//   CAD: 1.39,
//   HKD: 7.84,
//   DKK: 6.42,
//   HUF: 312.8,
//   ...
//   __base: "USD"
// }

__base records the currency the table was fetched for. Passing a table to convert(amount, from, to, rates) with a different from throws instead of returning a wrong number, and the key is an ordinary one so the check still works after the table has been through JSON.stringify and a cache. It is not a rate: skip it when iterating, or import RATES_BASE_KEY to name it. An underscore is not valid in a currency code, so it cannot collide with one.

This also allows for cached conversion:

import { Convert } from "easy-currencies";

const convert = await Convert().from("USD").fetch();

// use the fetched rates: (does not use the current provider's API anymore)
const value1 = await convert.amount(10).to("GBP");

await convert.from("USD").fetch(); // refresh rates
// or await convert.from("GBP").fetch() to switch base currency

const value2 = await convert.amount(10).to("GBP");

Using custom providers

Custom single provider initialization

import { Converter } from "easy-currencies";

const converter = new Converter("OpenExchangeRates", "API_KEY");
const value = await converter.convert(15, "USD", "EUR");

console.log(value); // converted value

Custom multiple provider initialization

import { Converter } from "easy-currencies";

const converter = new Converter(
  { name: "OpenExchangeRates", key: "API_KEY" },
  { name: "AlphaVantage", key: "API_KEY" },
  { name: "Fixer", key: "API_KEY" }
);
const value = await converter.convert(15, "USD", "EUR");
console.log(value); // converted value

Upgrading from 1.x? See MIGRATION.md.

Supported providers and API keys

The first column is the exact name to pass to new Converter().

The three keyless providers need no signup and are used by default: a new Converter() tries them in the order below, so a conversion keeps working when one of them is down. Nothing to configure.

They do not all cover the same currencies. ExchangeRateAPI carries 166 and Frankfurter carries 29, the ECB set, so a fallback changes both the rate source and which currencies resolve at all. A code that works most of the time can fail while the primary is down. Name a provider explicitly if you need one specific source.

Name Service API key
ExchangeRateAPI exchangerate-api.com not required
FloatRates floatrates.com not required
Frankfurter frankfurter.dev not required, ECB currencies only

The rest need a key from the provider. Naming one puts it first, ahead of the keyless providers, which stay on as fallbacks.

Name Service API key
ExchangeRatesAPIIO exchangeratesapi.io required
CurrencyLayer currencylayer.com required, with base currency support
OpenExchangeRates openexchangerates.org required
AlphaVantage alphavantage.co required
Fixer fixer.io required, with base currency support

ExchangeRateAPI and ExchangeRatesAPIIO are different services with confusingly similar names.

Using a proxy or a custom client

Requests go through the global fetch, which has no proxy option. Supply your own client to proxy, or to add an agent, retries or instrumentation.

If you already use axios, an axios instance satisfies the client interface as is, including the rejection behaviour the fallback chain depends on:

import axios from "axios";

converter.setClient(axios.create({ proxy: { host: "127.0.0.1", port: 8080 } }));

To stay dependency-free, use an undici dispatcher. A client must reject on an HTTP failure with the response attached. fetch resolves on 4xx and 5xx, so a client that does not check r.ok silently disables provider error mapping, the 429 retry and the fallback chain:

import { ProxyAgent } from "undici";

const dispatcher = new ProxyAgent("http://127.0.0.1:8080");

converter.setClient({
  get: async (url) => {
    const r = await fetch(url, { dispatcher });
    let data;
    try {
      data = await r.json();
    } catch {
      data = undefined;
    }
    const headers = Object.fromEntries(r.headers);

    if (!r.ok) {
      const err = new Error(`Request failed with status code ${r.status}`);
      err.response = { status: r.status, data, headers };
      throw err;
    }
    return { status: r.status, data, headers };
  }
});

A client is { get(url) } resolving to { status, data, headers? }. headers is optional and enables Retry-After handling on a 429. TypeScript users passing dispatcher to fetch need { dispatcher } as any; it is not in the DOM RequestInit type.

To keep the built-in client and change only the timeout:

import { createClient } from "easy-currencies";

converter.setClient(createClient({ timeout: 30000 })); // default is 10000

Bounding how long a conversion can take

A conversion gets 20 seconds of wall clock, spent across the whole fallback chain rather than reset for each provider, and it covers the requests themselves. A client that never settles cannot hold a conversion open past it.

converter.setRetryOptions({ budgetMs: 5000 }); // an HTTP handler
converter.setRetryOptions({ maxRetries: 0 }); // never retry a 429
Option Default Meaning
budgetMs 20000 wall clock for the whole call, across every provider
maxRetries 2 retries after the initial request, on a 429 only
maxDelay 8000 upper bound in ms on one backoff wait, before jitter

Fields merge, so one can be set without restating the rest.

API

Converter:

  • new Converter(...providers)
  • convert(amount, from, to, rates?)
  • getRates(from, to, multiple?)
  • convertRate(amount, to, rates)
  • add(name, provider, setActive?)
  • addMultiple(providers, setActive?)
  • remove(provider)
  • setClient(client)
  • setRetryOptions(options)

Convert(amount?), chainable:

  • .from(currency)
  • .amount(value)
  • .fetch()
  • .rates
  • .to(currency)

Full type signatures are in etc/easy-currencies.api.md.

The list of configured (active) providers can be accessed like so:

import { Converter } from "easy-currencies";

const converter = new Converter("OpenExchangeRates", "API_KEY");

console.log(converter.providers);
/**
 * [{
 *  endpoint: {
 *    base: "https://openexchangerates.org/api/latest.json?app_id=%KEY%",
 *    single: "&base=%FROM%",
 *  },
 *  key: "API_KEY",
 *  handler: function(data) {
 *    return data.rates;
 *  },
 *  errors: {
 *    401: "Invalid API key!"
 *  },
 *  errorHandler: function(data) {
 *    return data.status;
 *  }
 * },
 * // the keyless providers, added automatically as fallbacks (see below)
 * {
 *  endpoint: {
 *    base: "https://api.exchangerate-api.com/v4/latest/",
 *    single: "%FROM%",
 *  },
 *  key: undefined,
 *  handler: function(data) {
 *    return data.rates;
 *  },
 *  errors: {
 *    400: "Malformed query.",
 *    404: "Currency not found"
 *  },
 *  errorHandler: function(data) {
 *    return data.status;
 *  }
 * },
 * { // FloatRates, endpoint https://www.floatrates.com/daily/ },
 * { // Frankfurter, endpoint https://api.frankfurter.dev/v1/latest?base= }]
 */

The current active provider can be retrieved like this:

import { Converter } from "easy-currencies";

const converter = new Converter("OpenExchangeRates", "API_KEY");

console.log(converter.config.activeProvider()); // ...provider data
Automatic provider fallbacks

Upon creation of a converter, a default provider that does not require any API keys is automatically inserted into the list of active providers as a primary fallback. It always has lower priority than the providers the converter was initialized with.

If a provider is well defined(all possible errors are registered properly), a conversion error is reported through converter.onError and the conversion flow resumes with the next active provider. A failure applies to that call only: no provider is removed, so one unknown currency cannot degrade a long-lived converter.

If there are no more providers to fall back on, the converter throws the error. Moreover, if the error is not registered (unhandled error), it will be thrown as well.

Adding custom providers

Custom provider definitions can be added as such:

import { Converter } from "easy-currencies";

const converter = new Converter();

converter.add("MyProvider", {
  // the name of the custom provider
  endpoint: {
    base: "http://myprovider.net/api/live?access_key=%KEY%", // the base endpoint of the conversion API, with %KEY% being the api key's slot
    single: "&source=%FROM%" // the string that will be appended to the base endpoint, with %FROM% being the base currency abbreviation
  },
  key: "API_KEY", // your api key
  handler: function (data) {
    // the function that takes the JSON data returned by the API and returns the rate key-value object
    return data.rates;
  },
  errors: {
    // key-value object of common errors and their text representations
    101: "Invalid API key!",
    201: "Invalid base currency!"
  },
  errorHandler: function (data) {
    // runs on every response, success included, so it must tolerate a body with no error
    return data && data.error ? data.error.code : null;
  }
});

errorHandler always receives the response body, on a 200 and on an HTTP failure alike. If your vendor signals with a status code rather than in the body, key errors by the status and return null here: an HTTP status is matched against errors when the handler finds nothing.

Multiple providers can be added with addMultiple:

import { Converter } from "easy-currencies";

const converter = new Converter();

converter.addMultiple([
  { name: "Name1", provider: provider1 },
  { name: "Name2", provider: provider2 }
]);
Support

Submit bugs and feature requests through the project's issue tracker:

Issues

Keywords