# rust_api_calling
[](https://crates.io/crates/rust_api_calling)
[](https://docs.rs/rust_api_calling)
[](https://opensource.org/licenses/MIT)
A clean, idiomatic Rust HTTP client library with **one core function** that handles all HTTP communication. Built on top of [reqwest](https://crates.io/crates/reqwest) with automatic JSON serialization via [serde](https://crates.io/crates/serde).
## Features
- **Single core function** — `make_request` handles everything; `get()` and `post()` are thin wrappers
- **Typed responses** — Deserialize JSON directly into your Rust structs via generics
- **Builder pattern** — Configure the client once with `ApiClient::builder()`, reuse everywhere
- **Base URL support** — Set it once, then use relative paths like `"/users"`
- **Session management** — Automatic XSRF token and cookie handling across requests
- **Per-request overrides** — Custom timeout, headers, and bearer token per call via `RequestConfig`
- **Strongly-typed errors** — `ApiError` enum with clear variants instead of magic error codes
- **Structured logging** — Built-in [tracing](https://crates.io/crates/tracing) integration
- **Async/await** — Powered by [tokio](https://crates.io/crates/tokio)
## Installation
Add to your `Cargo.toml`:
```toml
[dependencies]
rust_api_calling = "0.1.0"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
```
## Quick Start
```rust
use rust_api_calling::{ApiClient, ApiResponse};
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct Post {
id: u32,
title: String,
body: String,
}
#[tokio::main]
async fn main() {
let client = ApiClient::builder()
.base_url("https://jsonplaceholder.typicode.com")
.build()
.unwrap();
// GET request — response is automatically deserialized into Post
let response: ApiResponse<Post> = client
.get("/posts/1", None, None)
.await
.unwrap();
println!("Post #{}: {}", response.body.id, response.body.title);
}
```
## Usage Guide
### 1. Create the Client
Create one `ApiClient` and reuse it — it uses connection pooling internally.
```rust
use rust_api_calling::ApiClient;
use std::time::Duration;
let client = ApiClient::builder()
.base_url("https://api.example.com") // Prepended to all relative paths
.default_timeout(Duration::from_secs(30)) // Default timeout for all requests
.default_header("Accept", "application/json") // Sent with every request
.session_enabled(true) // Auto-manage cookies & XSRF tokens
.build()
.unwrap();
```
### 2. GET Requests
```rust
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct User {
name: String,
email: String,
}
// Simple GET
let response = client.get::<Vec<User>>("/users", None, None).await?;
println!("Got {} users", response.body.len());
// GET with query parameters
let response = client
.get::<Vec<User>>("/users", Some(&[("role", "admin"), ("limit", "10")]), None)
.await?;
```
### 3. POST Requests
```rust
use serde::{Serialize, Deserialize};
#[derive(Serialize)]
struct CreateUser {
name: String,
email: String,
password: String,
}
#[derive(Deserialize)]
struct UserResponse {
id: String,
name: String,
}
let new_user = CreateUser {
name: "Keval".to_string(),
email: "keval@example.com".to_string(),
password: "secure123".to_string(),
};
let response = client
.post::<UserResponse, _>("/users", Some(&new_user), None)
.await?;
println!("Created user with ID: {}", response.body.id);
```
### 4. Per-Request Configuration
Override client defaults for individual requests:
```rust
use rust_api_calling::RequestConfig;
use std::time::Duration;
let config = RequestConfig::new()
.timeout(Duration::from_secs(5)) // Custom timeout
.bearer_token("eyJhbGciOiJIUzI1NiJ9...") // Authorization header
.header("X-Request-ID", "req-12345"); // Custom header
let response = client
.get::<serde_json::Value>("/protected/resource", None, Some(config))
.await?;
```
### 5. Error Handling
All errors are strongly typed via the `ApiError` enum:
```rust
use rust_api_calling::ApiError;
match client.get::<serde_json::Value>("/users/999", None, None).await {
Ok(response) => {
println!("Status: {}", response.status);
println!("Body: {:?}", response.body);
}
Err(ApiError::HttpError { status, body }) => {
// Server returned 4xx or 5xx
eprintln!("HTTP {}: {}", status, body);
}
Err(ApiError::Timeout) => {
eprintln!("Request timed out!");
}
Err(ApiError::NetworkError(e)) => {
eprintln!("Network error: {}", e);
}
Err(ApiError::InvalidUrl(url)) => {
eprintln!("Bad URL: {}", url);
}
Err(e) => {
eprintln!("Other error: {}", e);
}
}
```
### 6. Using Raw JSON Responses
If you don't want to define a struct, use `serde_json::Value`:
```rust
let response = client
.get::<serde_json::Value>("/users", None, None)
.await?;
// Access fields dynamically
if let Some(users) = response.body.as_array() {
for user in users {
println!("Name: {}", user["name"]);
}
}
// Raw body string is also available
println!("Raw JSON: {}", response.raw_body);
```
### 7. Absolute URLs (Bypass Base URL)
You can always pass a full URL to bypass the configured `base_url`:
```rust
// This ignores the base_url and calls the full URL directly
let response = client
.get::<serde_json::Value>("https://other-api.com/data", None, None)
.await?;
```
### 8. Session Management
When `session_enabled(true)` is set, the client automatically:
- Reads XSRF tokens from response headers (`x-xsrf-token`, `xsrf-token`, `x-csrf-token`)
- Reads cookies from `set-cookie` headers
- Attaches them to all subsequent requests
You can also manage sessions manually:
```rust
// Set session data manually
client.session.set_xsrf_token("my-token");
client.session.set_cookie("session=abc123");
// Read current session data
if let Some(token) = client.session.xsrf_token() {
println!("Current XSRF token: {}", token);
}
// Clear all session data
client.session.clear();
```
### 9. The Core Function
All methods delegate to `make_request`. You can call it directly for full control:
```rust
use rust_api_calling::HttpMethod;
let response = client
.make_request::<serde_json::Value, _>(
HttpMethod::Post,
"/endpoint",
Some(&my_body), // Request body (serialized to JSON)
Some(&[("key", "val")]), // Query parameters
Some(config), // Per-request config
)
.await?;
```
### 10. Response Object
Every response includes:
```rust
let response = client.get::<MyType>("/endpoint", None, None).await?;
response.status; // HTTP status code (u16)
response.headers; // HashMap<String, String>
response.body; // Deserialized body (MyType)
response.raw_body; // Raw response string
response.is_success(); // true if 2xx
response.header("content-type"); // Case-insensitive header lookup
```
## API Reference
### `ApiClient`
| `ApiClient::builder()` | Start building a new client |
| `.get(url, query, config)` | Perform a GET request |
| `.post(url, body, config)` | Perform a POST request |
| `.make_request(method, url, body, query, config)` | The core function — full control |
### `ApiClientBuilder`
| `.base_url(url)` | Set the base URL for all relative paths |
| `.default_timeout(duration)` | Set default timeout (default: 60s) |
| `.default_header(key, value)` | Add a header sent with every request |
| `.session_enabled(bool)` | Enable automatic session/cookie management |
| `.build()` | Build the `ApiClient` |
### `RequestConfig`
| `RequestConfig::new()` | Create empty per-request config |
| `.timeout(duration)` | Override timeout for this request |
| `.bearer_token(token)` | Set Authorization bearer token |
| `.header(key, value)` | Add a custom header |
### `ApiError`
| `InvalidUrl(String)` | URL could not be parsed |
| `NetworkError(reqwest::Error)` | Connection, DNS, or TLS failure |
| `SerializationError(serde_json::Error)` | JSON serialize/deserialize failure |
| `HttpError { status, body }` | Server returned non-2xx status |
| `EmptyResponse` | Server returned empty body |
| `Timeout` | Request timed out |
## Logging
This library uses the [tracing](https://crates.io/crates/tracing) crate for structured logging. To see log output, add a subscriber in your application:
```rust
// Add to dev-dependencies: tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tracing_subscriber::fmt()
.with_env_filter("rust_api_calling=debug")
.init();
```
Log levels used:
- `INFO` — Request method and URL
- `DEBUG` — Query params, request body, response body
- `WARN` — Empty responses
- `ERROR` — Network errors, timeouts, HTTP errors, deserialization failures
## Example
A full working example is included in the `examples/` directory:
```bash
cargo run --example usage
```
## License
Licensed under the [MIT License](LICENSE).