Introduction: Why Write an HTTP Client in Rust?
Modern development rarely goes without interaction with external services. Whether it's a weather REST API, a database, or a microservice architecture — the ability to send HTTP requests is a fundamental skill. Rust, with its emphasis on performance and safety, offers several elegant solutions for working with HTTP. In this article, we will break down how to properly create HTTP clients in Rust: from choosing a library to handling complex scenarios.
We will look at the two most popular libraries: reqwest (the asynchronous standard) and ureq (a simple synchronous approach). You will learn how to send GET and POST requests, process JSON, manage headers, and avoid common mistakes.
1. Choosing a Library: reqwest vs ureq
Before writing code, you need to decide on the tool. There are two main candidates in the Rust ecosystem.
1.1. Reqwest — Powerful and Asynchronous
reqwest is the de facto standard for HTTP requests in Rust. It is built on top of hyper and tokio, supports asynchrony, automatic JSON serialization/deserialization, and cookie handling. It is ideal for high-load applications and web servers.
To get started, add this to Cargo.toml:
[dependencies]reqwest = { version = "0.12", features = ["json"] }tokio = { version = "1", features = ["full"] }serde = { version = "1.0", features = ["derive"] }serde_json = "1.0"1.2. Ureq — Simplicity Without Asynchrony
If you don't need asynchronous calls and are writing a simple script or CLI utility, ureq is an excellent choice. It is synchronous, minimalistic, and does not require complex runtime setup.
[dependencies]ureq = "2.9"serde = { version = "1.0", features = ["derive"] }serde_json = "1.0"When to choose what? For web servers and complex applications — reqwest. For simple tasks, tests, and scripts — ureq.
2. Basic Requests: GET and POST
Let's look at practical examples for both libraries.
2.1. GET Request with reqwest
The simplest example is fetching data from a public API. We use an asynchronous main function with the #[tokio::main] macro.
use reqwest;use std::error::Error;
#[tokio::main]async fn main() -> Result<(), Box<dyn Error>> { let url = "https://jsonplaceholder.typicode.com/posts/1"; let response = reqwest::get(url).await?;
// Check the response status if response.status().is_success() { let body = response.text().await?; println!("Response: {}", body); } else { eprintln!("HTTP Error: {}", response.status()); }
Ok(())}2.2. POST Request with JSON Body (reqwest)
Sending data often requires serializing a struct. For this, we use the serde crate.
use reqwest;use serde::{Deserialize, Serialize};use std::error::Error;
#[derive(Serialize, Deserialize, Debug)]struct Post { title: String, body: String, user_id: i32,}
#[tokio::main]async fn main() -> Result<(), Box<dyn Error>> { let client = reqwest::Client::new(); let new_post = Post { title: "Hello Rust".to_string(), body: "This is the request body".to_string(), user_id: 1, };
let response = client .post("https://jsonplaceholder.typicode.com/posts") .json(&new_post) .send() .await?;
let created_post: Post = response.json().await?; println!("Created post: {:?}", created_post);
Ok(())}2.3. Simple GET with ureq
With ureq, the code becomes shorter since it doesn't require asynchrony:
use ureq;use std::error::Error;
fn