How do I make HTTP requests in Rust?

Walkthrough

The reqwest crate is a popular, ergonomic HTTP client for Rust. It provides both blocking and async APIs, supports HTTPS, cookies, JSON serialization, multipart forms, and more. Built on top of hyper and tokio, reqwest handles the complexity of HTTP while offering a simple, chainable interface. It's the de-facto standard for making HTTP requests in Rust applications.

Key features:

  1. Async and blocking clients — choose based on your needs
  2. Request builders — chainable methods to configure requests
  3. Automatic JSON — serialize/deserialize with serde
  4. Headers and cookies — full HTTP feature support
  5. TLS/HTTPS — secure connections by default
  6. Timeouts and redirects — configurable behavior

Reqwest integrates seamlessly with Tokio for async applications.

Code Example

# Cargo.toml
[dependencies]
reqwest = { version = "0.11", features = ["json"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
use reqwest::Error;
use serde::Deserialize;
 
#[derive(Deserialize)]
struct User {
    id: u32,
    name: String,
    email: String,
}
 
#[tokio::main]
async fn main() -> Result<(), Error> {
    // Simple GET request
    let response = reqwest::get("https://jsonplaceholder.typicode.com/users/1").await?;
    let user: User = response.json().await?;
    println!("User: {} ({})", user.name, user.email);
    Ok(())
}

Basic Requests

use reqwest::Error;
 
#[tokio::main]
async fn main() -> Result<(), Error> {
    // Simple GET request
    let body = reqwest::get("https://httpbin.org/get").await?.text().await?;
    println!("GET response: {}", body);
    
    // Get response details
    let response = reqwest::get("https://httpbin.org/get").await?;
    println!("Status: {}", response.status());
    println!("Headers: {:?}", response.headers());
    
    // Check status before proceeding
    let response = reqwest::get("https://httpbin.org/status/404").await?;
    if response.status().is_success() {
        let body = response.text().await?;
        println!("Success: {}", body);
    } else {
        println!("Error status: {}", response.status());
    }
    
    // POST request
    let client = reqwest::Client::new();
    let response = client
        .post("https://httpbin.org/post")
        .body("raw body content")
        .send()
        .await?;
    println!("POST response: {}", response.status());
    
    // PUT request
    let response = client
        .put("https://httpbin.org/put")
        .body("put body")
        .send()
        .await?;
    println!("PUT status: {}", response.status());
    
    // DELETE request
    let response = client
        .delete("https://httpbin.org/delete")
        .send()
        .await?;
    println!("DELETE status: {}", response.status());
    
    Ok(())
}

The Client and Request Builder

use reqwest::{Client, ClientBuilder, Error};
use std::time::Duration;
 
#[tokio::main]
async fn main() -> Result<(), Error> {
    // Create a client with custom settings
    let client = Client::builder()
        .timeout(Duration::from_secs(10))
        .connect_timeout(Duration::from_secs(5))
        .user_agent("MyApp/1.0")
        .build()?;
    
    // Reuse the client for multiple requests
    let resp1 = client.get("https://httpbin.org/get").send().await?;
    let resp2 = client.get("https://httpbin.org/get").send().await?;
    
    println!("Request 1: {}", resp1.status());
    println!("Request 2: {}", resp2.status());
    
    // Client with more options
    let client = ClientBuilder::new()
        .timeout(Duration::from_secs(30))
        .connect_timeout(Duration::from_secs(10))
        .pool_max_idle_per_host(10)      // Connection pool size
        .pool_idle_timeout(Duration::from_secs(60))
        .redirect(reqwest::redirect::Policy::limited(5))  // Follow up to 5 redirects
        .gzip(true)                       // Enable gzip
        .brotli(true)                     // Enable brotli
        .build()?;
    
    let response = client.get("https://httpbin.org/get").send().await?;
    println!("Custom client: {}", response.status());
    
    Ok(())
}
 
// Client with cookie support
#[cfg(feature = "cookies")]
fn cookie_client() -> Result<Client, Error> {
    let client = Client::builder()
        .cookie_store(true)  // Enable cookie store
        .build()?;
    Ok(client)
}

Query Parameters and Headers

use reqwest::{Client, header, Error};
 
#[tokio::main]
async fn main() -> Result<(), Error> {
    let client = Client::new();
    
    // Query parameters
    let response = client
        .get("https://httpbin.org/get")
        .query(&[
            ("page", "1"),
            ("limit", "10"),
            ("sort", "desc"),
        ])
        .send()
        .await?;
    println!("Query params: {}", response.status());
    
    // Query with struct (uses serde URL encoding)
    #[derive(serde::Serialize)]
    struct Params {
        page: u32,
        limit: u32,
        query: String,
    }
    
    let params = Params {
        page: 1,
        limit: 10,
        query: "rust".to_string(),
    };
    
    let response = client
        .get("https://httpbin.org/get")
        .query(&params)
        .send()
        .await?;
    println!("Struct params: {}", response.status());
    
    // Single header
    let response = client
        .get("https://httpbin.org/headers")
        .header("X-Custom-Header", "value")
        .send()
        .await?;
    println!("Custom header: {}", response.status());
    
    // Multiple headers
    let response = client
        .get("https://httpbin.org/headers")
        .header("X-API-Key", "secret-key")
        .header("X-Request-ID", "12345")
        .header(header::ACCEPT, "application/json")
        .header(header::USER_AGENT, "MyApp/1.0")
        .send()
        .await?;
    println!("Multiple headers: {}", response.status());
    
    // Header map
    let mut headers = header::HeaderMap::new();
    headers.insert("X-Auth-Token", "my-token".parse().unwrap());
    headers.insert(header::CONTENT_TYPE, "application/json".parse().unwrap());
    
    let response = client
        .get("https://httpbin.org/headers")
        .headers(headers)
        .send()
        .await?;
    println!("Header map: {}", response.status());
    
    Ok(())
}

Request Bodies

use reqwest::{Client, header, Error};
use serde::{Serialize, Deserialize};
 
#[tokio::main]
async fn main() -> Result<(), Error> {
    let client = Client::new();
    
    // Plain text body
    let response = client
        .post("https://httpbin.org/post")
        .header(header::CONTENT_TYPE, "text/plain")
        .body("Plain text body")
        .send()
        .await?;
    println!("Text body: {}", response.status());
    
    // JSON body from string
    let response = client
        .post("https://httpbin.org/post")
        .header(header::CONTENT_TYPE, "application/json")
        .body(r#"{"name": "Alice", "age": 30}"#)
        .send()
        .await?;
    println!("JSON string body: {}", response.status());
    
    // JSON body with json() method (requires json feature)
    #[derive(Serialize)]
    struct User {
        name: String,
        email: String,
    }
    
    let user = User {
        name: "Alice".to_string(),
        email: "alice@example.com".to_string(),
    };
    
    let response = client
        .post("https://httpbin.org/post")
        .json(&user)
        .send()
        .await?;
    println!("JSON struct body: {}", response.status());
    
    // Form data
    let response = client
        .post("https://httpbin.org/post")
        .form(&[
            ("username", "alice"),
            ("password", "secret"),
        ])
        .send()
        .await?;
    println!("Form body: {}", response.status());
    
    // Form from struct
    #[derive(Serialize)]
    struct LoginForm {
        username: String,
        password: String,
    }
    
    let form = LoginForm {
        username: "bob".to_string(),
        password: "secret123".to_string(),
    };
    
    let response = client
        .post("https://httpbin.org/post")
        .form(&form)
        .send()
        .await?;
    println!("Form struct body: {}", response.status());
    
    // Binary body
    let data = vec![0u8, 1, 2, 3, 4, 5];
    let response = client
        .post("https://httpbin.org/post")
        .body(data)
        .send()
        .await?;
    println!("Binary body: {}", response.status());
    
    // Body from file (requires async)
    // let file = tokio::fs::File::open("data.bin").await?;
    // let response = client
    //     .post("https://httpbin.org/post")
    //     .body(file)
    //     .send()
    //     .await?;
    
    Ok(())
}

Response Handling

use reqwest::{Client, Error, Response, StatusCode};
use serde::Deserialize;
 
#[tokio::main]
async fn main() -> Result<(), Error> {
    let client = Client::new();
    
    // Get response object
    let response = client.get("https://httpbin.org/get").send().await?;
    
    // Status code
    println!("Status: {}", response.status());
    println!("Is success: {}", response.status().is_success());
    println!("Is client error: {}", response.status().is_client_error());
    println!("Is server error: {}", response.status().is_server_error());
    
    // Headers
    for (name, value) in response.headers() {
        println!("{}: {:?}", name, value);
    }
    
    // Get specific header
    if let Some(content_type) = response.headers().get("content-type") {
        println!("Content-Type: {:?}", content_type);
    }
    
    // Response body as text
    let text = response.text().await?;
    println!("Body length: {} bytes", text.len());
    
    // Response body as bytes
    let response = client.get("https://httpbin.org/bytes/100").send().await?;
    let bytes = response.bytes().await?;
    println!("Bytes: {} bytes", bytes.len());
    
    // JSON response
    #[derive(Deserialize, Debug)]
    struct HttpBinResponse {
        url: String,
        headers: std::collections::HashMap<String, String>,
    }
    
    let response = client.get("https://httpbin.org/get").send().await?;
    let json: HttpBinResponse = response.json().await?;
    println!("JSON response: {:?}", json);
    
    // Check for error status
    let response = client.get("https://httpbin.org/status/404").send().await?;
    match response.error_for_status() {
        Ok(resp) => println!("Success: {}", resp.status()),
        Err(err) => println!("Error: {}", err),
    }
    
    // error_for_status_ref doesn't consume response
    let response = client.get("https://httpbin.org/get").send().await?;
    if let Err(err) = response.error_for_status_ref() {
        println!("Request failed: {}", err);
    } else {
        let body = response.text().await?;
        println!("Body: {}", body.chars().take(100).collect::<String>());
    }
    
    Ok(())
}
 
// Helper function for error handling
async fn fetch_json<T: for<'de> Deserialize<'de>>(url: &str) -> Result<T, Box<dyn std::error::Error>> {
    let response = reqwest::get(url).await?;
    
    if !response.status().is_success() {
        return Err(format!("HTTP error: {}", response.status()).into());
    }
    
    let data: T = response.json().await?;
    Ok(data)
}

Timeouts and Error Handling

use reqwest::{Client, Error, StatusCode};
use std::time::Duration;
 
#[tokio::main]
async fn main() -> Result<(), Error> {
    // Client with timeouts
    let client = Client::builder()
        .timeout(Duration::from_secs(10))        // Total request timeout
        .connect_timeout(Duration::from_secs(5))  // Connection timeout
        .read_timeout(Duration::from_secs(5))     // Read timeout
        .build()?;
    
    // Request with timeout
    let response = client
        .get("https://httpbin.org/delay/2")
        .timeout(Duration::from_secs(5))
        .send()
        .await?;
    println!("Response: {}", response.status());
    
    // Error handling
    match fetch_with_error_handling("https://httpbin.org/status/500").await {
        Ok(body) => println!("Success: {}", body),
        Err(e) => println!("Error: {}", e),
    }
    
    Ok(())
}
 
async fn fetch_with_error_handling(url: &str) -> Result<String, Box<dyn std::error::Error>> {
    let client = Client::builder()
        .timeout(Duration::from_secs(5))
        .build()?;
    
    let response = client.get(url).send().await?;
    
    // Check status code
    match response.status() {
        StatusCode::OK => {
            let body = response.text().await?;
            Ok(body)
        }
        StatusCode::NOT_FOUND => {
            Err("Resource not found".into())
        }
        StatusCode::INTERNAL_SERVER_ERROR => {
            Err("Server error".into())
        }
        status if status.is_client_error() => {
            Err(format!("Client error: {}", status).into())
        }
        status if status.is_server_error() => {
            Err(format!("Server error: {}", status).into())
        }
        _ => {
            let body = response.text().await?;
            Ok(body)
        }
    }
}
 
// Comprehensive error handling
async fn robust_fetch(url: &str) -> Result<String, reqwest::Error> {
    let response = reqwest::get(url).await?;  // Network errors
    
    // Convert HTTP errors to reqwest::Error
    let response = response.error_for_status()?;
    
    let body = response.text().await?;  // Body read errors
    Ok(body)
}
 
// With retry logic
async fn fetch_with_retry(url: &str, max_retries: u32) -> Result<String, Box<dyn std::error::Error>> {
    let client = Client::builder()
        .timeout(Duration::from_secs(10))
        .build()?;
    
    let mut retries = 0;
    
    loop {
        match client.get(url).send().await {
            Ok(response) => {
                if response.status().is_success() {
                    return Ok(response.text().await?);
                } else if response.status().is_server_error() && retries < max_retries {
                    retries += 1;
                    tokio::time::sleep(Duration::from_millis(100 * retries as u64)).await;
                    continue;
                } else {
                    return Err(format!("HTTP error: {}", response.status()).into());
                }
            }
            Err(e) => {
                if retries < max_retries {
                    retries += 1;
                    tokio::time::sleep(Duration::from_millis(100 * retries as u64)).await;
                    continue;
                }
                return Err(e.into());
            }
        }
    }
}

Authentication

use reqwest::{Client, header, Error};
 
#[tokio::main]
async fn main() -> Result<(), Error> {
    let client = Client::new();
    
    // Basic authentication
    let response = client
        .get("https://httpbin.org/basic-auth/user/pass")
        .basic_auth("user", Some("pass"))
        .send()
        .await?;
    println!("Basic auth: {}", response.status());
    
    // Bearer token
    let token = "my-secret-token";
    let response = client
        .get("https://httpbin.org/bearer")
        .bearer_auth(token)
        .send()
        .await?;
    println!("Bearer auth: {}", response.status());
    
    // Custom Authorization header
    let response = client
        .get("https://httpbin.org/headers")
        .header(header::AUTHORIZATION, "Token my-api-key")
        .send()
        .await?;
    println!("Custom auth header: {}", response.status());
    
    // API key in header
    let response = client
        .get("https://httpbin.org/headers")
        .header("X-API-Key", "your-api-key")
        .send()
        .await?;
    println!("API key header: {}", response.status());
    
    Ok(())
}
 
// Authenticated client wrapper
struct ApiClient {
    client: Client,
    base_url: String,
}
 
impl ApiClient {
    fn new(base_url: &str, token: &str) -> Result<Self, Error> {
        let mut headers = header::HeaderMap::new();
        let auth_value = format!("Bearer {}", token);
        headers.insert(
            header::AUTHORIZATION,
            auth_value.parse().unwrap(),
        );
        
        let client = Client::builder()
            .default_headers(headers)
            .build()?;
        
        Ok(Self {
            client,
            base_url: base_url.to_string(),
        })
    }
    
    async fn get<T: serde::de::DeserializeOwned>(&self, path: &str) -> Result<T, Error> {
        let url = format!("{}{}", self.base_url, path);
        self.client.get(&url).send().await?.json().await
    }
    
    async fn post<T: serde::Serialize, R: serde::de::DeserializeOwned>(
        &self,
        path: &str,
        body: &T,
    ) -> Result<R, Error> {
        let url = format!("{}{}", self.base_url, path);
        self.client
            .post(&url)
            .json(body)
            .send()
            .await?
            .json()
            .await
    }
}

Multipart Form Uploads

use reqwest::{Client, multipart, Error};
 
#[tokio::main]
async fn main() -> Result<(), Error> {
    let client = Client::new();
    
    // Simple multipart form
    let form = multipart::Form::new()
        .text("username", "alice")
        .text("email", "alice@example.com");
    
    let response = client
        .post("https://httpbin.org/post")
        .multipart(form)
        .send()
        .await?;
    println!("Multipart form: {}", response.status());
    
    // File upload
    let file_content = "This is file content";
    let part = multipart::Part::text(file_content)
        .file_name("document.txt")
        .mime_str("text/plain")?;
    
    let form = multipart::Form::new()
        .part("file", part)
        .text("description", "My document");
    
    let response = client
        .post("https://httpbin.org/post")
        .multipart(form)
        .send()
        .await?;
    println!("File upload: {}", response.status());
    
    // Multiple files
    let form = multipart::Form::new()
        .text("title", "Multiple files")
        .part("file1", multipart::Part::text("Content 1")
            .file_name("file1.txt"))
        .part("file2", multipart::Part::text("Content 2")
            .file_name("file2.txt"));
    
    let response = client
        .post("https://httpbin.org/post")
        .multipart(form)
        .send()
        .await?;
    println!("Multiple files: {}", response.status());
    
    // Binary file upload
    let binary_data = vec![0u8, 1, 2, 3, 4, 5];
    let part = multipart::Part::bytes(binary_data)
        .file_name("data.bin")
        .mime_str("application/octet-stream")?;
    
    let form = multipart::Form::new()
        .part("binary", part);
    
    let response = client
        .post("https://httpbin.org/post")
        .multipart(form)
        .send()
        .await?;
    println!("Binary upload: {}", response.status());
    
    Ok(())
}
 
// Upload file from disk
async fn upload_file(client: &Client, path: &str) -> Result<(), Error> {
    let file = tokio::fs::read(path).await?;
    let file_name = std::path::Path::new(path)
        .file_name()
        .unwrap()
        .to_str()
        .unwrap();
    
    let part = multipart::Part::bytes(file)
        .file_name(file_name.to_string());
    
    let form = multipart::Form::new()
        .part("file", part);
    
    let response = client
        .post("https://httpbin.org/post")
        .multipart(form)
        .send()
        .await?;
    
    println!("Uploaded {}", file_name);
    Ok(())
}

Streaming Responses

use reqwest::{Client, Error};
use futures::StreamExt;
 
#[tokio::main]
async fn main() -> Result<(), Error> {
    let client = Client::new();
    
    // Stream response body as bytes
    let response = client
        .get("https://httpbin.org/stream-bytes/1000")
        .send()
        .await?;
    
    let mut stream = response.bytes_stream();
    let mut total = 0;
    
    while let Some(chunk) = stream.next().await {
        let chunk = chunk?;
        total += chunk.len();
        println!("Received {} bytes (total: {})", chunk.len(), total);
    }
    
    // Stream to file
    let response = client
        .get("https://httpbin.org/bytes/10000")
        .send()
        .await?;
    
    let mut file = tokio::fs::File::create("downloaded.bin").await?;
    let mut stream = response.bytes_stream();
    
    use tokio::io::AsyncWriteExt;
    while let Some(chunk) = stream.next().await {
        let chunk = chunk?;
        file.write_all(&chunk).await?;
    }
    
    println!("Download complete");
    
    // Clean up
    tokio::fs::remove_file("downloaded.bin").await?;
    
    Ok(())
}
 
// Download with progress
async fn download_with_progress(
    url: &str,
    output_path: &str,
) -> Result<(), Box<dyn std::error::Error>> {
    let client = Client::new();
    let response = client.get(url).send().await?;
    
    let total_size = response.content_length().unwrap_or(0);
    println!("Downloading {} bytes", total_size);
    
    let mut file = tokio::fs::File::create(output_path).await?;
    let mut stream = response.bytes_stream();
    let mut downloaded = 0u64;
    
    use tokio::io::AsyncWriteExt;
    
    while let Some(chunk) = stream.next().await {
        let chunk = chunk?;
        file.write_all(&chunk).await?;
        downloaded += chunk.len() as u64;
        
        if total_size > 0 {
            let percent = (downloaded as f64 / total_size as f64) * 100.0;
            print!("\rProgress: {:.1}%", percent);
        }
    }
    
    println!("\nDownload complete!");
    Ok(())
}

Async Concurrent Requests

use reqwest::{Client, Error};
use futures::future;
 
#[tokio::main]
async fn main() -> Result<(), Error> {
    let client = Client::new();
    
    // Multiple concurrent requests
    let urls = vec![
        "https://httpbin.org/delay/1",
        "https://httpbin.org/delay/2",
        "https://httpbin.org/delay/1",
    ];
    
    // Create futures for all requests
    let requests: Vec<_> = urls
        .iter()
        .map(|url| client.get(*url).send())
        .collect();
    
    // Execute all requests concurrently
    let responses = future::join_all(requests).await;
    
    for (i, result) in responses.into_iter().enumerate() {
        match result {
            Ok(resp) => println!("Request {}: {}", i, resp.status()),
            Err(e) => println!("Request {} error: {}", i, e),
        }
    }
    
    Ok(())
}
 
// Fetch multiple URLs with results
async fn fetch_all(urls: &[&str]) -> Vec<Result<String, reqwest::Error>> {
    let client = Client::new();
    
    let requests: Vec<_> = urls
        .iter()
        .map(|url| {
            let client = &client;
            async move {
                let resp = client.get(*url).send().await?;
                let text = resp.text().await?;
                Ok::<_, reqwest::Error>(text)
            }
        })
        .collect();
    
    futures::future::join_all(requests).await
}
 
// Concurrent with limit
async fn fetch_with_limit(urls: &[&str], concurrency: usize) -> Vec<Result<String, reqwest::Error>> {
    use futures::stream::{self, StreamExt};
    
    let client = Client::new();
    
    let results = stream::iter(urls)
        .map(|url| {
            let client = &client;
            async move {
                let resp = client.get(*url).send().await?;
                let text = resp.text().await?;
                Ok::<_, reqwest::Error>(text)
            }
        })
        .buffer_unordered(concurrency)
        .collect::<Vec<_>>()
        .await;
    
    results
}

Blocking Client

# Cargo.toml
[dependencies]
reqwest = { version = "0.11", features = ["blocking", "json"] }
use reqwest::blocking::Client;
use reqwest::Error;
use serde::Deserialize;
 
fn main() -> Result<(), Error> {
    // Simple blocking GET
    let body = reqwest::blocking::get("https://httpbin.org/get")?.text()?;
    println!("Response: {}", &body[..100.min(body.len())]);
    
    // Blocking client
    let client = Client::new();
    
    // GET request
    let response = client
        .get("https://httpbin.org/get")
        .query(&[("key", "value")])
        .send()?;
    println!("GET: {}", response.status());
    
    // POST with JSON
    #[derive(serde::Serialize, Deserialize)]
    struct User {
        name: String,
        email: String,
    }
    
    let user = User {
        name: "Alice".to_string(),
        email: "alice@example.com".to_string(),
    };
    
    let response = client
        .post("https://httpbin.org/post")
        .json(&user)
        .send()?;
    println!("POST: {}", response.status());
    
    // JSON response
    let response = client
        .get("https://jsonplaceholder.typicode.com/users/1")
        .send()?;
    let user: User = response.json()?;
    println!("User: {}", user.name);
    
    Ok(())
}

Real-World API Client

use reqwest::{Client, Error, StatusCode};
use serde::{Deserialize, Serialize};
use std::time::Duration;
 
#[derive(Debug, Serialize, Deserialize)]
struct Post {
    id: u32,
    title: String,
    body: String,
    userId: u32,
}
 
#[derive(Debug, Serialize)]
struct CreatePost {
    title: String,
    body: String,
    userId: u32,
}
 
struct JsonPlaceholderApi {
    client: Client,
    base_url: String,
}
 
impl JsonPlaceholderApi {
    fn new() -> Result<Self, Error> {
        let client = Client::builder()
            .timeout(Duration::from_secs(10))
            .build()?;
        
        Ok(Self {
            client,
            base_url: "https://jsonplaceholder.typicode.com".to_string(),
        })
    }
    
    async fn get_posts(&self) -> Result<Vec<Post>, Error> {
        let url = format!("{}/posts", self.base_url);
        self.client
            .get(&url)
            .send()
            .await?
            .json()
            .await
    }
    
    async fn get_post(&self, id: u32) -> Result<Option<Post>, Error> {
        let url = format!("{}/posts/{}", self.base_url, id);
        let response = self.client
            .get(&url)
            .send()
            .await?;
        
        if response.status() == StatusCode::NOT_FOUND {
            return Ok(None);
        }
        
        let post: Post = response.json().await?;
        Ok(Some(post))
    }
    
    async fn create_post(&self, post: CreatePost) -> Result<Post, Error> {
        let url = format!("{}/posts", self.base_url);
        self.client
            .post(&url)
            .json(&post)
            .send()
            .await?
            .json()
            .await
    }
    
    async fn update_post(&self, id: u32, post: CreatePost) -> Result<Post, Error> {
        let url = format!("{}/posts/{}", self.base_url, id);
        self.client
            .put(&url)
            .json(&post)
            .send()
            .await?
            .json()
            .await
    }
    
    async fn delete_post(&self, id: u32) -> Result<bool, Error> {
        let url = format!("{}/posts/{}", self.base_url, id);
        let response = self.client
            .delete(&url)
            .send()
            .await?;
        
        Ok(response.status().is_success())
    }
    
    async fn get_posts_by_user(&self, user_id: u32) -> Result<Vec<Post>, Error> {
        let url = format!("{}/posts", self.base_url);
        self.client
            .get(&url)
            .query(&[("userId", user_id)])
            .send()
            .await?
            .json()
            .await
    }
}
 
#[tokio::main]
async fn main() -> Result<(), Error> {
    let api = JsonPlaceholderApi::new()?;
    
    // Get all posts
    let posts = api.get_posts().await?;
    println!("Found {} posts", posts.len());
    
    // Get single post
    if let Some(post) = api.get_post(1).await? {
        println!("Post 1: {}", post.title);
    }
    
    // Create post
    let new_post = CreatePost {
        title: "My Post".to_string(),
        body: "Post content".to_string(),
        userId: 1,
    };
    let created = api.create_post(new_post).await?;
    println!("Created post with ID: {}", created.id);
    
    // Get posts by user
    let user_posts = api.get_posts_by_user(1).await?;
    println!("User 1 has {} posts", user_posts.len());
    
    Ok(())
}

Summary

  • Use reqwest::get(url).await? for simple GET requests
  • Use Client::new() for reusable client with connection pooling
  • Use .json(&data) for JSON request bodies (requires json feature)
  • Use .form(&data) for form-encoded requests
  • Use .query(&params) for query parameters
  • Use .header(name, value) to add headers
  • Use .timeout(Duration) for request timeouts
  • Use .bearer_auth(token) or .basic_auth(user, pass) for authentication
  • Use response.json::<T>().await? to deserialize JSON responses
  • Use response.text().await? for text, response.bytes().await? for bytes
  • Use response.error_for_status()? to convert HTTP errors to Result::Err
  • Use multipart::Form for file uploads
  • Use response.bytes_stream() for streaming responses
  • Use futures::future::join_all for concurrent requests
  • Use reqwest::blocking for synchronous requests (requires blocking feature)
  • Configure client with Client::builder() for timeouts, headers, cookies
  • HTTPS is enabled by default with native TLS
  • Use .multipart(form) for multipart/form-data uploads