@aibulat/restclients/worldbank
api.worldbank.org — every country, the classifications they belong to, and ~25000 development indicators. No key, no rate limit, and it is a government dataset, so it does not disappear.
import {WorldBankApi, rowsOf} from '@aibulat/restclients/worldbank';
const api = new WorldBankApi();
const res = await api.getCountries({perPage: 300});
const countries = rowsOf(await res.json());
console.log(countries.length); // 296Two things are unlike every other client here
Every response is a tuple, not a named envelope: pagination metadata, then the rows.
[{"page": 1, "pages": 6, "per_page": 50, "total": 296}, [ ... ]]A failure is still HTTP 200. An unknown country code produces:
[{"message": [{"id": "120", "key": "Invalid value", "value": "The provided parameter value is not valid"}]}]and nothing rejects. That is why the helpers below exist, and why methods are typed as WorldBankPayload<T> — the union of both shapes — so you cannot reach the rows without passing through something that considers the failure case.
They take the parsed body, not the response, so they stay synchronous now that reading the body is an explicit step: rowsOf(await res.json()).
rowsOf(body): Array<T> // the rows, or throws if the body was an error
metaOf(body): WorldBankMeta | undefined
hasMore(body): boolean
isErrorBody(body): boolean // type guard
errorMessage(body): stringconst res = await api.getCountry('ZZZ');
const body = await res.json();
res.status; // 200
rowsOf(body); // throws: World Bank API error -- Invalid value: ...What it throws is a WorldBankError, so the case can be narrowed rather than matched on its message. It is not an HttpError: nothing failed at the HTTP layer, and messages carries the API's own list, ids and all.
import {WorldBankError} from '@aibulat/restclients/worldbank';
try {
rowsOf(body);
}
catch (err) {
if (err instanceof WorldBankError) {
console.log(err.messages[0].key); // 'Invalid value'
}
}The client also sends format=json on every request as an instance default, because the API answers in XML otherwise, on every endpoint, forever.
Methods
getCountries(options?, config?)
getCountry(code, config?) // ISO alpha-2 or alpha-3
getRegions(options?, config?)
getIncomeLevels(options?, config?)
getLendingTypes(options?, config?)
getSources(options?, config?)
getIndicators(options?, config?)
getIndicator(indicator, config?)
getCountryIndicator(code, indicator, options?, config?)Paging is shared by all of them — {page?, perPage?}, default 50 per page. getCountries also takes region, incomeLevel and lendingType filters:
rowsOf(await api.getCountries({region: 'LCN', perPage: 100}));getCountryIndicator takes {date, mrv, gapfill, frequency} on top of paging:
const series = rowsOf(await api.getCountryIndicator('PER', 'SP.POP.TOTL', {date: '2000:2023'}));
series[0]; // {indicator: {...}, country: {...}, date: '2023', value: 33845617, ...}code may be several codes joined with a semicolon ('PER;CHL'), or 'all' for every country. Observations come back newest first, and value is null for years with no data — common at the edges of a series, since most indicators have nothing for the current year yet.
Useful indicator ids: SP.POP.TOTL (population), NY.GDP.MKTP.CD (GDP, current US$), SP.DYN.LE00.IN (life expectancy).
Types
WorldBankCountry, Classification (the shape /region, /incomeLevel and /lendingType share), Indicator, Observation, Source, plus CodeValue for the {id, iso2code, value} pairs used throughout.
type WorldBankResponse<T> = [WorldBankMeta, Array<T>];
type WorldBankErrorBody = [{message: Array<{id, key, value}>}];
type WorldBankPayload<T> = WorldBankResponse<T> | WorldBankErrorBody;WorldBankMeta types its numbers as number | string. That is not defensiveness — /region genuinely sends strings where /country sends numbers. hasMore() coerces before comparing, so you do not have to.
Notes
- The country list includes aggregates. Around 70 of the 296 rows are regions and income groups, not countries. Filter with
regionto exclude them, or checkregion.valueagainst'Aggregates'. getIndicatorsis 25000 rows across hundreds of pages. Page through it rather than trying to fetch it all.