@drfrost/bods-js
Version:
JavaScript client for the UK's Bus Open Data Service (BODS) API
187 lines (161 loc) • 5.04 kB
text/typescript
import type { ApiConfig, ErrorResponse } from '../types/common.js';
/**
* HTTP response wrapper
*/
export interface HttpResponse<T> {
data: T;
status: number;
statusText: string;
headers: Headers;
}
/**
* HTTP client error
*/
export class HttpClientError extends Error {
constructor(
message: string,
public status: number,
public response?: any
) {
super(message);
this.name = 'HttpClientError';
}
}
/**
* Base HTTP client for BODS API requests
*/
export class HttpClient {
private readonly apiKey: string;
private readonly baseUrl: string;
private readonly timeout: number;
constructor(config: ApiConfig) {
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
*/
private buildUrl(endpoint: string, params: Record<string, any> = {}): string {
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
*/
private async request<T>(
endpoint: string,
options: RequestInit & { params?: Record<string, any> } = {}
): Promise<HttpResponse<T>> {
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: ErrorResponse | string;
try {
errorData = await response.json() as ErrorResponse;
} 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: T;
if (contentType?.includes('application/json')) {
data = await response.json() as T;
} else if (contentType?.includes('text/xml') || contentType?.includes('application/xml')) {
data = await response.text() as T;
} else if (contentType?.includes('application/protobuf') || contentType?.includes('application/octet-stream')) {
data = await response.arrayBuffer() as T;
} else {
data = await response.text() as T;
}
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<T>(endpoint: string, params?: Record<string, any>): Promise<HttpResponse<T>> {
return this.request<T>(endpoint, { method: 'GET', params });
}
/**
* Perform POST request
*/
async post<T>(endpoint: string, body?: any, params?: Record<string, any>): Promise<HttpResponse<T>> {
return this.request<T>(endpoint, {
method: 'POST',
params,
body: JSON.stringify(body),
headers: {
'Content-Type': 'application/json',
},
});
}
/**
* Perform PUT request
*/
async put<T>(endpoint: string, body?: any, params?: Record<string, any>): Promise<HttpResponse<T>> {
return this.request<T>(endpoint, {
method: 'PUT',
params,
body: JSON.stringify(body),
headers: {
'Content-Type': 'application/json',
},
});
}
/**
* Perform DELETE request
*/
async delete<T>(endpoint: string, params?: Record<string, any>): Promise<HttpResponse<T>> {
return this.request<T>(endpoint, { method: 'DELETE', params });
}
}