/**
* Census API Client
*
* Infrastructure layer for interacting with the US Census Bureau API.
* Handles HTTP communication, error handling, and response parsing.
*/
import { CensusApiResponse, CensusApiConfig } from '../../domain/types.js';
import { ILogger } from '../logging/logger.js';
/**
* Client interface for Census API
* Allows for easy mocking and testing
*/
export interface ICensusApiClient {
getPopulationData(stateQuery: string): Promise<CensusApiResponse | null>;
}
/**
* HTTP-based Census API client implementation
*/
export class CensusApiClient implements ICensusApiClient {
private readonly config: CensusApiConfig;
private readonly logger: ILogger;
constructor(config: CensusApiConfig, logger: ILogger) {
this.config = config;
this.logger = logger;
}
/**
* Fetch population data from Census API
*
* @param stateQuery - State query string (e.g., "*", "6", "1,6,36")
* @returns Census API response or null on failure
*/
async getPopulationData(
stateQuery: string
): Promise<CensusApiResponse | null> {
const url = this.buildUrl(stateQuery);
this.logger.debug('Fetching Census data', {
url,
stateQuery,
});
try {
const response = await this.fetchWithTimeout(url);
if (!response.ok) {
this.logger.error(
'Census API HTTP error',
new Error(`HTTP ${response.status}: ${response.statusText}`),
{
status: response.status,
statusText: response.statusText,
url,
}
);
return null;
}
const data = (await response.json()) as CensusApiResponse;
this.logger.info('Census data retrieved successfully', {
rowCount: data.length,
stateQuery,
});
return data;
} catch (error) {
this.logger.error(
'Failed to fetch Census data',
error instanceof Error ? error : new Error(String(error)),
{ url, stateQuery }
);
return null;
}
}
/**
* Build Census API URL with query parameters
*
* @param stateQuery - State query string
* @returns Complete API URL
*/
private buildUrl(stateQuery: string): string {
const params = new URLSearchParams({
get: 'NAME,P1_001N',
for: `state:${stateQuery}`,
});
return `${this.config.baseUrl}?${params.toString()}`;
}
/**
* Fetch with timeout support
*
* @param url - URL to fetch
* @returns Fetch response
*/
private async fetchWithTimeout(url: string): Promise<Response> {
const timeout = this.config.timeout || 10000;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
signal: controller.signal,
});
return response;
} finally {
clearTimeout(timeoutId);
}
}
}
/**
* Mock Census API client for testing
*/
export class MockCensusApiClient implements ICensusApiClient {
private mockData: CensusApiResponse | null;
constructor(mockData: CensusApiResponse | null = null) {
this.mockData = mockData;
}
async getPopulationData(): Promise<CensusApiResponse | null> {
return Promise.resolve(this.mockData);
}
setMockData(data: CensusApiResponse | null): void {
this.mockData = data;
}
}
/**
* Factory function to create Census API client
*/
export function createCensusApiClient(
logger: ILogger,
baseUrl: string = 'https://api.census.gov/data/2020/dec/pl',
timeout?: number
): ICensusApiClient {
const config: CensusApiConfig = {
baseUrl,
timeout,
};
return new CensusApiClient(config, logger);
}