Expand description
RESO Web API Client Library
A Rust client library for RESO Web API servers that implement the OData 4.0 protocol. This library provides a type-safe, ergonomic interface for querying real estate data from MLS systems.
§Features
- 🔍 Fluent Query Builder - Build complex OData queries with a clean, fluent API
- 🔐 OAuth Authentication - Built-in support for bearer token authentication
- 📊 Full OData Support - Filter, sort, paginate, select fields, expand relations
- 🔢 Count Queries - Efficient record counting via
/$countendpoint - 🗂️ Dataset ID Support - Handle RESO servers that use dataset identifiers
- 📖 Metadata Retrieval - Fetch and parse OData
$metadatadocuments - 🔄 Replication Endpoint - Bulk data transfer with up to 2000 records/request
- ⚡ Async/Await - Built on tokio for high-performance concurrent operations
- 🛡️ Type-Safe Errors - Comprehensive error types with detailed context
§Stability
This library is pre-1.0, meaning the API may change between minor versions. It follows semantic versioning, but breaking changes may occur in 0.x releases.
§Quick Start
use reso_client::{ResoClient, QueryBuilder};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create client from environment variables
// Requires: RESO_BASE_URL and RESO_TOKEN
let client = ResoClient::from_env()?;
// Build and execute a query
let query = QueryBuilder::new("Property")
.filter("City eq 'Austin' and ListPrice gt 500000")
.select(&["ListingKey", "City", "ListPrice", "BedroomsTotal"])
.order_by("ListPrice", "desc")
.top(10)
.build()?;
let results = client.execute(&query).await?;
// Access the records from the OData response
if let Some(records) = results["value"].as_array() {
println!("Found {} properties", records.len());
for record in records {
let key = record["ListingKey"].as_str().unwrap_or("");
let price = record["ListPrice"].as_f64().unwrap_or(0.0);
println!("{}: ${}", key, price);
}
}
Ok(())
}§Configuration
The client can be configured via environment variables or programmatically:
§Environment Variables
RESO_BASE_URL=https://api.bridgedataoutput.com/api/v2/OData
RESO_TOKEN=your-oauth-token
RESO_DATASET_ID=actris_ref # Optional
RESO_TIMEOUT=30 # Optional, seconds§Manual Configuration
let config = ClientConfig::new(
"https://api.mls.com/odata",
"your-bearer-token"
)
.with_dataset_id("actris_ref")
.with_timeout(Duration::from_secs(60));
let client = ResoClient::with_config(config)?;§Common Usage Patterns
§Filtering with OData Expressions
// Simple equality
let query = QueryBuilder::new("Property")
.filter("City eq 'Austin'")
.build()?;
// Multiple conditions
let query = QueryBuilder::new("Property")
.filter("City eq 'Austin' and ListPrice gt 500000 and BedroomsTotal ge 3")
.build()?;
// String functions
let query = QueryBuilder::new("Property")
.filter("startswith(City, 'San')")
.build()?;
// Date comparison
let query = QueryBuilder::new("Property")
.filter("ModificationTimestamp gt 2025-01-01T00:00:00Z")
.build()?;§Pagination
// First page
let query = QueryBuilder::new("Property")
.top(20)
.build()?;
let page1 = client.execute(&query).await?;
// Second page
let query = QueryBuilder::new("Property")
.skip(20)
.top(20)
.build()?;
let page2 = client.execute(&query).await?;§Getting Total Counts
// Include count in response (with records)
let query = QueryBuilder::new("Property")
.filter("City eq 'Austin'")
.with_count()
.top(10)
.build()?;
let results = client.execute(&query).await?;
if let Some(count) = results["@odata.count"].as_u64() {
println!("Total matching records: {}", count);
}
// Count only (no records, more efficient)
let query = QueryBuilder::new("Property")
.filter("City eq 'Austin'")
.count()
.build()?;
let count = client.execute_count(&query).await?;
println!("Total: {}", count);§Bulk Data with Replication
let query = ReplicationQueryBuilder::new("Property")
.filter("StandardStatus eq 'Active'")
.select(&["ListingKey", "City", "ListPrice"])
.top(2000) // Max: 2000 for replication
.build()?;
let mut response = client.execute_replication(&query).await?;
let mut all_records = response.records;
// Continue fetching while next link is available
while let Some(next_link) = response.next_link {
response = client.execute_next_link(&next_link).await?;
all_records.extend(response.records);
}
println!("Total records fetched: {}", all_records.len());§Error Handling
let client = ResoClient::from_env()?;
let query = QueryBuilder::new("Property").top(10).build()?;
match client.execute(&query).await {
Ok(results) => {
println!("Success!");
}
Err(ResoError::Unauthorized { message, .. }) => {
eprintln!("Authentication failed: {}", message);
}
Err(ResoError::NotFound { message, .. }) => {
eprintln!("Resource not found: {}", message);
}
Err(ResoError::RateLimited { message, .. }) => {
eprintln!("Rate limited: {}", message);
}
Err(ResoError::Network(msg)) => {
eprintln!("Network error: {}", msg);
}
Err(e) => {
eprintln!("Other error: {}", e);
}
}§Stability
This library is pre-1.0, meaning the API may change between minor versions. We follow semantic versioning, but breaking changes may occur in 0.x releases. We strive to keep changes minimal and well-documented in the CHANGELOG.
§Resources
Re-exports§
pub use client::ClientConfig;pub use client::ResoClient;pub use error::ResoError;pub use error::Result;pub use queries::Query;pub use queries::QueryBuilder;pub use queries::ReplicationQuery;pub use queries::ReplicationQueryBuilder;pub use replication::ReplicationResponse;
Modules§
- client
- Client configuration and connection management
- error
- Error types for the RESO client library
- queries
- Query building for RESO/OData requests
- replication
- Replication endpoint response types
Enums§
- Json
Value - Represents any valid JSON value.