TL;DR: Reqwest is Rust's most-used HTTP client crate; an ergonomic, async wrapper around the lower-level Hyper library. It handles connection pooling, TLS, redirects, cookies, and JSON serialization automatically. For most Rust applications that need to call external APIs or fetch web content, Reqwest is the default choice. It integrates natively with Tokio and Serde.
What Is Reqwest?
Reqwest is a high-level, async HTTP client for Rust; the standard choice for making HTTP requests to external APIs and web services.
# Cargo.toml
[dependencies]
reqwest = { version = "0.12", features = ["json"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
// Simple GET request
let body = reqwest::get("https://httpbin.org/get")
.await?
.text()
.await?;
println!("{body}");
Ok(())
}How Do You Make JSON Requests and Parse Responses?
Reqwest integrates with Serde; .json::<T>() deserializes the response body directly into a Rust type, and .json(&payload) serializes a struct as the request body.
use reqwest::Client;
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Debug)]
struct ApiUser {
id: u64,
name: String,
email: String,
}
#[derive(Serialize)]
struct CreateUser {
name: String,
email: String,
}
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
let client = Client::new();
// GET and deserialize JSON
let user: ApiUser = client
.get("https://jsonplaceholder.typicode.com/users/1")
.send()
.await?
.json::<ApiUser>()
.await?;
println!("{:?}", user);
// POST with JSON body
let new_user = CreateUser {
name: "Alice".to_string(),
email: "[email protected]".to_string(),
};
let response = client
.post("https://jsonplaceholder.typicode.com/users")
.json(&new_user) // serializes to JSON, sets Content-Type
.send()
.await?;
println!("Status: {}", response.status());
Ok(())
}How Do You Use a Reusable Client?
Create a Client once and reuse it; it manages a connection pool internally. Creating a new client per request wastes TCP connections.
use reqwest::{Client, header};
use std::time::Duration;
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
let mut headers = header::HeaderMap::new();
headers.insert(
header::AUTHORIZATION,
header::HeaderValue::from_static("Bearer my-api-token"),
);
let client = Client::builder()
.default_headers(headers) // sent with every request
.timeout(Duration::from_secs(10)) // request timeout
.connection_verbose(false)
.build()?;
// Reuse client for multiple requests; shares connection pool
let r1 = client.get("https://api.example.com/users").send().await?;
let r2 = client.get("https://api.example.com/posts").send().await?;
println!("{} {}", r1.status(), r2.status());
Ok(())
}In Axum or Actix applications, store the Client in your application state (Arc<AppState>) and clone it per request; Client::clone is cheap (shared connection pool).
How Do You Handle Errors and Status Codes?
.send() only errors on network failures. HTTP error status codes (4xx, 5xx) are not errors; use .error_for_status() to convert them.
use reqwest::Client;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
let response = client
.get("https://httpbin.org/status/404")
.send()
.await?;
// Without this, a 404 is NOT an error
println!("{}", response.status()); // 404 Not Found
// error_for_status(); converts 4xx/5xx to Err
let response = client
.get("https://httpbin.org/status/404")
.send()
.await?
.error_for_status()?; // returns Err for 404
Ok(())
}Frequently Asked Questions
Async by default. There is a reqwest::blocking module for synchronous code, but the async API is recommended for all new code that runs inside a Tokio runtime.
Hyper is the low-level HTTP library Reqwest is built on. Hyper gives you full control but requires manual request/response handling. Reqwest adds connection pooling, TLS, redirects, JSON serialization, and a friendlier API. Use Reqwest for application code; use Hyper if you're building a framework or need byte-level HTTP control.
Use .form(&data) instead of .json(&data); it encodes the data as application/x-www-form-urlencoded. For multipart/form-data (file uploads), use reqwest::multipart::Form.
let client = reqwest::Client::builder()
.proxy(reqwest::Proxy::https("http://proxy:8080")?)
.build()?;Or set the HTTPS_PROXY environment variable; Reqwest respects standard proxy environment variables automatically.
Sources
Related Glossary Terms
- Async/Await: Reqwest is async-first and requires a Tokio runtime
- Tokio: The async runtime Reqwest is built on
- Serde: Reqwest uses Serde for JSON request/response handling
- Result: All Reqwest operations return
Result<T, reqwest::Error> - Rustls: TLS in Reqwest is commonly handled through rustls
- Regex: HTTP clients often pair with regex for response parsing and scraping workflows
- Hyper: Reqwest is the high-level client layer built on top of Hyper's HTTP stack
Keep Reading
- Building AI Agents in Rust: reqwest powers HTTP calls to LLM APIs in Rust agents
- Rust on AWS Lambda: making outbound HTTP requests from serverless Rust
- Rust at Cloudflare: HTTP clients in edge computing

