@drfrost/bods-js
Version:
JavaScript client for the UK's Bus Open Data Service (BODS) API
151 lines (150 loc) • 4.93 kB
JavaScript
/**
* HTTP client error
*/
export class HttpClientError extends Error {
constructor(message, status, response) {
super(message);
this.status = status;
this.response = response;
this.name = 'HttpClientError';
}
}
/**
* Base HTTP client for BODS API requests
*/
export class HttpClient {
constructor(config) {
this.apiKey = config.apiKey;
this.baseUrl = config.baseUrl || 'https://data.bus-data.dft.gov.uk';
this.timeout = config.timeout || 30000;
}
/**
* Build URL with query parameters
*/
buildUrl(endpoint, params = {}) {
const url = new URL(endpoint, this.baseUrl);
// Add API key
url.searchParams.set('api_key', this.apiKey);
// Add other parameters
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
if (Array.isArray(value)) {
// Handle array parameters (comma-separated)
url.searchParams.set(key, value.join(','));
}
else if (value instanceof Date) {
// Handle date parameters
url.searchParams.set(key, value.toISOString());
}
else {
url.searchParams.set(key, String(value));
}
}
});
return url.toString();
}
/**
* Make HTTP request with timeout and error handling
*/
async request(endpoint, options = {}) {
const { params, ...fetchOptions } = options;
const url = this.buildUrl(endpoint, params);
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
try {
const response = await fetch(url, {
...fetchOptions,
signal: controller.signal,
headers: {
'Accept': 'application/json',
...fetchOptions.headers,
},
});
clearTimeout(timeoutId);
if (!response.ok) {
let errorData;
try {
errorData = await response.json();
}
catch {
errorData = await response.text();
}
const message = typeof errorData === 'object'
? errorData.detail
: `HTTP ${response.status}: ${response.statusText}`;
throw new HttpClientError(message, response.status, errorData);
}
const contentType = response.headers.get('content-type');
let data;
if (contentType?.includes('application/json')) {
data = await response.json();
}
else if (contentType?.includes('text/xml') || contentType?.includes('application/xml')) {
data = await response.text();
}
else if (contentType?.includes('application/protobuf') || contentType?.includes('application/octet-stream')) {
data = await response.arrayBuffer();
}
else {
data = await response.text();
}
return {
data,
status: response.status,
statusText: response.statusText,
headers: response.headers,
};
}
catch (error) {
clearTimeout(timeoutId);
if (error instanceof HttpClientError) {
throw error;
}
if (error instanceof Error) {
if (error.name === 'AbortError') {
throw new HttpClientError('Request timeout', 408);
}
throw new HttpClientError(error.message, 0);
}
throw new HttpClientError('Unknown error occurred', 0);
}
}
/**
* Perform GET request
*/
async get(endpoint, params) {
return this.request(endpoint, { method: 'GET', params });
}
/**
* Perform POST request
*/
async post(endpoint, body, params) {
return this.request(endpoint, {
method: 'POST',
params,
body: JSON.stringify(body),
headers: {
'Content-Type': 'application/json',
},
});
}
/**
* Perform PUT request
*/
async put(endpoint, body, params) {
return this.request(endpoint, {
method: 'PUT',
params,
body: JSON.stringify(body),
headers: {
'Content-Type': 'application/json',
},
});
}
/**
* Perform DELETE request
*/
async delete(endpoint, params) {
return this.request(endpoint, { method: 'DELETE', params });
}
}