import { httpClient } from './http-client.js';
import { dataTransformer } from './data-transformer.js';
import { appConfig } from '../config/index.js';
import { Property, SearchQuery, ZillowPropertyResponse } from '../types/real-estate.js';
import { APIError, DataSourceError, logError } from '../utils/errors.js';
/**
* Zillow API service using RapidAPI
*/
export class ZillowService {
private readonly baseUrl = 'https://zillow-com1.p.rapidapi.com';
private readonly headers = {
'x-rapidapi-host': appConfig.rapidApiHostZillow,
'x-rapidapi-key': appConfig.rapidApiKey,
};
/**
* Search properties using Zillow API
*/
async searchProperties(query: SearchQuery): Promise<Property[]> {
try {
console.log(`Searching Zillow for properties: ${query.location}`);
// Transform our search query to Zillow API parameters
const zillowParams = this.buildZillowSearchParams(query);
const response = await httpClient.get<any>(
`${this.baseUrl}/propertyExtendedSearch`,
{
headers: this.headers,
params: zillowParams,
provider: 'zillow',
}
);
// Handle different response structures from Zillow API
const properties = this.extractPropertiesFromResponse(response);
// Transform to our Property interface
const transformedProperties = properties.map(prop =>
dataTransformer.transformZillowProperty(prop)
);
console.log(`Found ${transformedProperties.length} properties from Zillow`);
return transformedProperties;
} catch (error) {
logError(error as Error, 'Zillow Search');
if (error instanceof APIError) {
throw new DataSourceError(
`Zillow search failed: ${error.message}`,
'Zillow',
true // fallback available
);
}
throw error;
}
}
/**
* Get detailed property information by Zillow property ID
*/
async getPropertyDetails(zpid: string): Promise<Property | null> {
try {
console.log(`Getting Zillow property details for ZPID: ${zpid}`);
const response = await httpClient.get<any>(
`${this.baseUrl}/property`,
{
headers: this.headers,
params: { zpid },
provider: 'zillow',
}
);
if (!response || !response.zpid) {
return null;
}
const property = dataTransformer.transformZillowProperty(response);
console.log(`Retrieved property details for ${property.address.street || 'Unknown Address'}`);
return property;
} catch (error) {
logError(error as Error, 'Zillow Property Details');
if (error instanceof APIError && error.statusCode === 404) {
return null; // Property not found
}
throw new DataSourceError(
`Failed to get property details from Zillow: ${error}`,
'Zillow'
);
}
}
/**
* Get property photos
*/
async getPropertyPhotos(zpid: string): Promise<string[]> {
try {
const response = await httpClient.get<any>(
`${this.baseUrl}/images`,
{
headers: this.headers,
params: { zpid },
provider: 'zillow',
}
);
// Extract photo URLs from response
const photos: string[] = [];
if (response.images) {
response.images.forEach((img: any) => {
if (img.url) {
photos.push(img.url);
}
});
}
return photos;
} catch (error) {
logError(error as Error, 'Zillow Photos');
return []; // Return empty array on error, photos are not critical
}
}
/**
* Get property price history
*/
async getPriceHistory(zpid: string): Promise<any[]> {
try {
const response = await httpClient.get<any>(
`${this.baseUrl}/priceHistory`,
{
headers: this.headers,
params: { zpid },
provider: 'zillow',
}
);
return response.priceHistory || [];
} catch (error) {
logError(error as Error, 'Zillow Price History');
return []; // Return empty array on error
}
}
/**
* Get comparable properties (comps)
*/
async getComparableProperties(zpid: string): Promise<Property[]> {
try {
const response = await httpClient.get<any>(
`${this.baseUrl}/similarProperties`,
{
headers: this.headers,
params: { zpid },
provider: 'zillow',
}
);
if (!response.comparables) {
return [];
}
return response.comparables.map((comp: ZillowPropertyResponse) =>
dataTransformer.transformZillowProperty(comp)
);
} catch (error) {
logError(error as Error, 'Zillow Comparables');
return [];
}
}
/**
* Build Zillow API parameters from our SearchQuery
*/
private buildZillowSearchParams(query: SearchQuery): Record<string, any> {
const params: Record<string, any> = {
location: query.location,
};
if (query.minPrice) params.price_min = query.minPrice;
if (query.maxPrice) params.price_max = query.maxPrice;
if (query.minBeds) params.beds_min = query.minBeds;
if (query.minBaths) params.baths_min = query.minBaths;
if (query.minSqft) params.sqft_min = query.minSqft;
if (query.maxSqft) params.sqft_max = query.maxSqft;
if (query.propertyType) {
params.home_type = this.mapPropertyTypeToZillow(query.propertyType);
}
// Default to first page if not specified
params.page = query.page || 1;
// Limit results to prevent excessive API usage
params.limit = 25;
return params;
}
/**
* Map our property types to Zillow's expected values
*/
private mapPropertyTypeToZillow(propertyType: string): string {
const typeMap: Record<string, string> = {
'single_family': 'Houses',
'condo': 'Condos',
'townhouse': 'Townhomes',
'apartment': 'Apartments',
'multi_family': 'Multi-family',
'land': 'Lots',
'manufactured': 'Manufactured',
};
return typeMap[propertyType.toLowerCase()] || propertyType;
}
/**
* Extract properties array from various Zillow API response structures
*/
private extractPropertiesFromResponse(response: any): ZillowPropertyResponse[] {
// Handle different response structures that Zillow API might return
if (response.results) {
return Array.isArray(response.results) ? response.results : [];
}
if (response.searchResults && response.searchResults.listResults) {
return response.searchResults.listResults;
}
if (response.props) {
return Array.isArray(response.props) ? response.props : [];
}
if (Array.isArray(response)) {
return response;
}
// If it's a single property object, wrap it in an array
if (response.zpid) {
return [response];
}
console.warn('Unknown Zillow API response structure:', Object.keys(response));
return [];
}
/**
* Check if Zillow service is available
*/
async healthCheck(): Promise<boolean> {
try {
// Try a minimal API call to check if service is responsive
await httpClient.get(
`${this.baseUrl}/property`,
{
headers: this.headers,
params: { zpid: '123456789' }, // Use a dummy ZPID
provider: 'zillow',
cacheTtl: 60, // Short cache for health checks
}
);
return true; // Any response (even 404) means service is up
} catch (error) {
const apiError = error as APIError;
// 404 is expected for dummy ZPID, means service is working
if (apiError.statusCode === 404) {
return true;
}
// Other errors indicate service issues
return false;
}
}
}
// Singleton instance
export const zillowService = new ZillowService();