title: "" description: "Reference documentation for Navius " category: "Reference" tags: ["documentation", "reference"] last_updated: "April 3, 2025" version: "1.0"
API Resource Abstraction
This document explains the API resource abstraction pattern used in our project, which provides a unified way to handle API resources with built-in reliability features.
Overview
The API resource abstraction provides a clean, consistent pattern for handling external API interactions with the following features:
- Automatic caching: Resources are cached to reduce latency and external API calls
- Retry mechanism: Failed API calls are retried with exponential backoff
- Consistent error handling: All API errors are handled in a consistent way
- Standardized logging: API interactions are logged with consistent format
- Type safety: Strong typing ensures correctness at compile time
Core Components
The abstraction consists of the following components:
- ApiResource trait: Interface that resources must implement
- ApiHandlerOptions: Configuration options for handlers
- create_api_handler: Factory function to create Axum handlers with reliability features
- Support functions: Caching and retry helpers
Using the Pattern
1. Implementing ApiResource for your model
#![allow(unused)] fn main() { use crate::utils::api_resource::ApiResource; // Your model structure #[derive(Debug, Clone, Serialize, Deserialize)] struct User { id: i64, name: String, email: String, } // Implement ApiResource for your model impl ApiResource for User { type Id = i64; // The type of the ID field fn resource_type() -> &'static str { "user" // Used for caching and logging } fn api_name() -> &'static str { "UserService" // Used for logging } } }
2. Creating a Fetch Function
#![allow(unused)] fn main() { async fn fetch_user(state: &Arc<AppState>, id: i64) -> Result<User> { let url = format!("{}/users/{}", state.config.user_service_url, id); // Create a closure that returns the actual request future let fetch_fn = || async { state.client.get(&url).send().await }; // Make the API call using the common logger/handler api_logger::api_call("UserService", &url, fetch_fn, "User", id).await } }
3. Creating an API Handler
#![allow(unused)] fn main() { pub async fn get_user_handler( State(state): State<Arc<AppState>>, Path(id): Path<String>, ) -> Result<Json<User>> { // Define the fetch function inline to avoid lifetime issues let fetch_fn = move |state: &Arc<AppState>, id: i64| -> futures::future::BoxFuture<'static, Result<User>> { let state = state.clone(); // Clone the state to avoid lifetime issues Box::pin(async move { // Your actual API call logic here // ... }) }; // Create an API handler with reliability features let handler = create_api_handler( fetch_fn, ApiHandlerOptions { use_cache: true, use_retries: true, max_retry_attempts: 3, cache_ttl_seconds: 300, detailed_logging: true, }, ); // Execute the handler handler(State(state), Path(id)).await } }
Configuration Options
The ApiHandlerOptions
struct provides the following configuration options:
#![allow(unused)] fn main() { struct ApiHandlerOptions { use_cache: bool, // Whether to use caching use_retries: bool, // Whether to retry failed requests max_retry_attempts: u32, // Maximum number of retry attempts (default: 3) cache_ttl_seconds: u64, // Cache time-to-live in seconds (default: 300) detailed_logging: bool, // Whether to log detailed information (default: true) } }
Best Practices
- Keep fetch functions simple: They should focus on the API call logic
- Use consistent naming: Name conventions help with maintenance
- Add appropriate logging: Additional context helps with debugging
- Handle errors gracefully: Return appropriate error codes to clients
- Test thoroughly: Verify behavior with unit tests for each handler
Example Use Cases
Basic Handler with Default Options
#![allow(unused)] fn main() { pub async fn get_product_handler( State(state): State<Arc<AppState>>, Path(id): Path<String>, ) -> Result<Json<Product>> { create_api_handler( fetch_product, ApiHandlerOptions { use_cache: true, use_retries: true, max_retry_attempts: 3, cache_ttl_seconds: 300, detailed_logging: true, }, )(State(state), Path(id)).await } }
Custom Handler with Specific Options
#![allow(unused)] fn main() { pub async fn get_weather_handler( State(state): State<Arc<AppState>>, Path(location): Path<String>, ) -> Result<Json<Weather>> { create_api_handler( fetch_weather, ApiHandlerOptions { use_cache: true, // Weather data can be cached use_retries: false, // Weather requests shouldn't retry max_retry_attempts: 1, cache_ttl_seconds: 60, // Weather data changes frequently detailed_logging: false, // High volume endpoint, reduce logging }, )(State(state), Path(location)).await } }
Troubleshooting
Cache Not Working
If caching isn't working as expected:
- Verify the
use_cache
option is set totrue
- Ensure the
ApiResource
implementation is correct - Check if the cache is enabled in the application state
Retries Not Working
If retries aren't working as expected:
- Verify the
use_retries
option is set totrue
- Check the error type (only service errors are retried)
- Inspect the logs for retry attempts
Extending the Abstraction
This section explains how to extend the API resource abstraction to support new resource types beyond the existing ones.
Current Limitations
The current implementation has specialized type conversions for certain resource types, but it's designed to be extended.
Adding Support for a New Resource Type
1. Identify Your Resource Type
For example, let's say you want to add support for a new Product
type:
#![allow(unused)] fn main() { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Product { id: i64, name: String, price: f64, } }
2. Implement the ApiResource Trait
#![allow(unused)] fn main() { impl ApiResource for Product { type Id = i64; fn resource_type() -> &'static str { "product" } fn api_name() -> &'static str { "ProductAPI" } } }
3. Update the Type Conversions
Modify the type conversion functions in src/utils/api_resource/core.rs
:
#![allow(unused)] fn main() { fn convert_cached_resource<R: ApiResource>(cached: impl Any) -> Option<R> { // existing code for other types... // Handle Product resources else if type_id == std::any::TypeId::of::<Product>() { if let Some(product) = cached.downcast_ref::<Product>() { let boxed: Box<dyn Any> = Box::new(product.clone()); let resource_any: Box<dyn Any> = boxed; if let Ok(typed) = resource_any.downcast::<R>() { return Some(*typed); } } } None } }
4. Update the Cache Type if Needed
Depending on your needs, you may need to update the cache structure to handle multiple resource types.
Future Enhancements
Planned enhancements to the pattern include:
- Generic cache implementation that can work with any resource type
- Circuit breaker pattern for automatically handling failing services
- Integration with distributed tracing
- Dynamic configuration of retry and caching policies
Related Documents
- API Standards - API design guidelines
- Error Handling - Error handling patterns