ZetCode

Rust HTTP client

last modified November 13, 2026

In this article we send HTTP requests in Rust with the reqwest crate.

The Hypertext Transfer Protocol (HTTP) is the protocol of the web and of most web APIs. A client sends a request with a method such as GET or POST, a URL, headers, and an optional body; the server answers with a status code, headers, and a body.

The Rust standard library has TCP sockets but no HTTP client. reqwest is the most popular high-level HTTP client crate. It is built on hyper and Tokio, supports HTTPS, JSON, redirects, cookies, and proxies, and offers both an async API and a blocking one. The examples were tested with Rust 1.97, the 2024 edition, reqwest 0.13.5, Tokio 1.54.1, and serde_json 1.0.154. They are run as Cargo projects with cargo run -q and send requests to example.com and to httpbin.org, a service that echoes requests back as JSON.

Setting up the project

Since version 0.13, several reqwest features are opt-in. We enable the blocking client, JSON bodies, query strings, and form bodies. The async examples need Tokio, and serde parses the JSON responses into structs.

Cargo.toml
[package]
name = "http_example"
version = "0.1.0"
edition = "2024"

[dependencies]
reqwest = { version = "0.13.5", features = ["blocking", "form", "json", "query"] }
serde = { version = "1.0.229", features = ["derive"] }
serde_json = "1.0.154"
tokio = { version = "1.54.1", features = ["full"] }

The tokio, serde, and serde_json crates are added with cargo add as well. Without the query feature, the query method does not exist, and the compiler reports error E0599. HTTPS works out of the box; the default TLS backend in 0.13 is rustls.

$ cargo add reqwest --features blocking,json,query,form
    Updating crates.io index
      Adding reqwest v0.13.5 to dependencies
             Features:
             + __rustls
             + __rustls-aws-lc-rs
             + __tls
             + blocking
             + charset
             + default-tls
             + form
             + http2
             + json
             + query
             + rustls
             + system-proxy
             - __native-tls
             - __native-tls-alpn
             - brotli
             - cookies
             - deflate
             - gzip
             - hickory-dns
             - http3
             - multipart
             - native-tls
             - native-tls-no-alpn
             - native-tls-vendored
             - native-tls-vendored-no-alpn
             - rustls-no-provider
             - socks
             - stream
             - zstd
    Updating crates.io index
     Locking 177 packages to latest Rust 1.97.1 compatible versions

A blocking GET request

The blocking API is the simplest way to send a request. It needs no async runtime in our code.

main.rs
fn main() -> Result<(), reqwest::Error> {
    let resp = reqwest::blocking::get("https://example.com")?;

    println!("status: {}", resp.status());
    println!("version: {:?}", resp.version());
    println!("content-type: {:?}", resp.headers().get("content-type"));

    let body = resp.text()?;
    println!("length: {}", body.len());

    if let Some(start) = body.find("<title>")
        && let Some(end) = body.find("</title>")
    {
        println!("title: {}", &body[start + 7..end]);
    }

    Ok(())
}

We download the home page of example.com.

let resp = reqwest::blocking::get("https://example.com")?;

The get function sends a GET request and waits for the response headers. It is a shortcut for creating a client and sending one request. The function fails only when the request cannot be made; an error status such as 404 is still a successful response.

println!("status: {}", resp.status());
println!("version: {:?}", resp.version());

The status code and the protocol version are known as soon as the headers arrive. The connection used HTTP/2, negotiated during the TLS handshake.

let body = resp.text()?;

The text method reads the whole body and decodes it as text, using the charset from the Content-Type header. It consumes the response.

$ cargo run -q
status: 200 OK
version: HTTP/2.0
content-type: Some("text/html; charset=utf-8")
length: 577
title: Example Domain

An async GET request

The async API has the same shape, with an .await after each operation that waits for the network.

main.rs
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
    let resp = reqwest::get("https://httpbin.org/robots.txt").await?;
    println!("status: {}", resp.status());
    println!("length: {:?}", resp.content_length());

    let body = resp.text().await?;
    print!("{body}");

    Ok(())
}

We fetch a small text file.

let resp = reqwest::get("https://httpbin.org/robots.txt").await?;

The async reqwest::get function is used inside a Tokio runtime. The blocking client actually runs an async client on a background thread, so it must not be used inside async code.

println!("length: {:?}", resp.content_length());

The content_length method returns the size announced by the server, if any.

$ cargo run -q
status: 200 OK
length: Some(30)
User-agent: *
Disallow: /deny

Status codes

The status code tells whether a request succeeded. reqwest represents it with the StatusCode type.

main.rs
use reqwest::StatusCode;

fn main() -> Result<(), reqwest::Error> {
    let client = reqwest::blocking::Client::new();

    for code in [200, 404, 503] {
        let url = format!("https://httpbin.org/status/{code}");
        let resp = client.get(&url).send()?;
        let status = resp.status();
        println!(
            "{} success={} client_error={} server_error={}",
            status,
            status.is_success(),
            status.is_client_error(),
            status.is_server_error()
        );

        if status == StatusCode::NOT_FOUND {
            println!("  the page does not exist");
        }
    }

    let resp = client.get("https://httpbin.org/status/404").send()?;
    match resp.error_for_status() {
        Ok(r) => println!("ok: {}", r.status()),
        Err(e) => println!("error: {e} (status {:?})", e.status()),
    }

    Ok(())
}

We request three status codes from httpbin.

let client = reqwest::blocking::Client::new();

A Client holds a connection pool and its configuration. Creating one per program and reusing it is much faster than calling reqwest::get repeatedly, because connections are kept alive between requests.

status.is_success(),
status.is_client_error(),
status.is_server_error()

These methods test the class of the code: 2xx, 4xx, and 5xx. Specific codes can be compared with constants such as StatusCode::NOT_FOUND.

match resp.error_for_status() {

The error_for_status method turns 4xx and 5xx responses into errors. It is handy in a chain of ? operators.

$ cargo run -q
200 OK success=true client_error=false server_error=false
404 Not Found success=false client_error=true server_error=false
  the page does not exist
503 Service Unavailable success=false client_error=false server_error=true
error: HTTP status client error (404 Not Found) for url (https://httpbin.org/status/404) (status Some(404))

Request and response headers

Headers carry metadata about requests and responses.

main.rs
use reqwest::header::{ACCEPT, HeaderMap, HeaderValue, USER_AGENT};
use serde_json::Value;

fn main() -> Result<(), reqwest::Error> {
    let client = reqwest::blocking::Client::new();

    let resp = client
        .get("https://httpbin.org/headers")
        .header(USER_AGENT, "rust-http-example/1.0")
        .header(ACCEPT, "application/json")
        .header("X-Api-Version", "2")
        .send()?;

    let v: Value = resp.json()?;
    let sent = &v["headers"];
    for name in ["User-Agent", "Accept", "X-Api-Version", "Host"] {
        println!("{name}: {}", sent[name]);
    }

    let resp = client.get("https://httpbin.org/response-headers?Cache-Control=no-cache").send()?;
    let headers: &HeaderMap = resp.headers();
    println!("cache-control: {:?}", headers.get("cache-control"));
    println!("content-type: {:?}", headers.get("content-type").and_then(|h| h.to_str().ok()));
    println!("has etag: {}", headers.contains_key("etag"));
    println!("{} headers in total", headers.len());

    let value = HeaderValue::from_str("bad\nvalue");
    println!("{value:?}");

    Ok(())
}

We send custom headers and read the headers of a response.

.header(USER_AGENT, "rust-http-example/1.0")
.header(ACCEPT, "application/json")
.header("X-Api-Version", "2")

The header method adds a header to the request. Standard header names are available as constants; custom ones can be strings. reqwest sends no User-Agent by default; many APIs require one.

let headers: &HeaderMap = resp.headers();
println!("cache-control: {:?}", headers.get("cache-control"));

Header names are case-insensitive. The get method returns an Option<&HeaderValue>; to_str converts a value to a string slice if it is visible ASCII.

let value = HeaderValue::from_str("bad\nvalue");

A header value cannot contain a newline; otherwise an attacker could inject extra headers. The constructor rejects such values.

$ cargo run -q
User-Agent: "rust-http-example/1.0"
Accept: "application/json"
X-Api-Version: "2"
Host: "httpbin.org"
cache-control: Some("no-cache")
content-type: Some("application/json")
has etag: false
8 headers in total
Err(InvalidHeaderValue)

Query parameters

The query method appends URL-encoded parameters to the request URL.

main.rs
use serde::Serialize;
use serde_json::Value;

#[derive(Serialize)]
struct Search<'a> {
    q: &'a str,
    page: u32,
    #[serde(skip_serializing_if = "Option::is_none")]
    lang: Option<&'a str>,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = reqwest::blocking::Client::new();

    let resp = client
        .get("https://httpbin.org/get")
        .query(&[("name", "John Doe"), ("occupation", "gardener & cook")])
        .send()?;
    println!("{}", resp.url());
    let v: Value = resp.json()?;
    println!("{}", v["args"]);

    let params = Search { q: "rust async", page: 2, lang: None };
    let resp = client.get("https://httpbin.org/get").query(&params).send()?;
    println!("{}", resp.url());

    let mut url = reqwest::Url::parse("https://httpbin.org/get?page=1")?;
    url.query_pairs_mut().append_pair("sort", "date");
    println!("{url}");

    Ok(())
}

We pass parameters as an array of pairs and as a struct.

.query(&[("name", "John Doe"), ("occupation", "gardener & cook")])

The values are encoded: the space becomes + and the ampersand %26. Building the query string by hand with format! would get this wrong.

let params = Search { q: "rust async", page: 2, lang: None };
let resp = client.get("https://httpbin.org/get").query(&params).send()?;

Any type that implements Serialize can be the query. The skipped None field is not sent.

let mut url = reqwest::Url::parse("https://httpbin.org/get?page=1")?;
url.query_pairs_mut().append_pair("sort", "date");

The re-exported Url type can also build URLs directly. Its parse error is not a reqwest::Error, so main returns Box<dyn Error>.

$ cargo run -q
https://httpbin.org/get?name=John+Doe&occupation=gardener+%26+cook
{"name":"John Doe","occupation":"gardener & cook"}
https://httpbin.org/get?q=rust+async&page=2
https://httpbin.org/get?page=1&sort=date

JSON responses

With the json feature, the json method deserializes the response body into any type that implements Deserialize.

main.rs
use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct Slide {
    title: String,
    #[serde(default)]
    items: Vec<String>,
}

#[derive(Debug, Deserialize)]
struct Slideshow {
    author: String,
    title: String,
    slides: Vec<Slide>,
}

#[derive(Debug, Deserialize)]
struct Document {
    slideshow: Slideshow,
}

#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
    let doc: Document = reqwest::get("https://httpbin.org/json").await?.json().await?;

    let show = doc.slideshow;
    println!("{} by {}", show.title, show.author);
    for (i, slide) in show.slides.iter().enumerate() {
        println!("{}. {} ({} items)", i + 1, slide.title, slide.items.len());
    }

    Ok(())
}

The httpbin /json endpoint returns a sample slideshow document.

let doc: Document = reqwest::get("https://httpbin.org/json").await?.json().await?;

The target type is given by the annotation. Fields not declared in the structs are ignored. A body that does not match the structs is reported as a decode error.

#[serde(default)]
items: Vec<String>,

The first slide has no items key; the default makes it an empty vector. Working with JSON is covered in more detail in Rust Serde JSON.

$ cargo run -q
Sample Slide Show by Yours Truly
1. Wake up to WonderWidgets! (0 items)
2. Overview (2 items)

Posting JSON

The json method of the request builder serializes a value into the request body.

main.rs
use serde::{Deserialize, Serialize};
use serde_json::Value;

#[derive(Serialize)]
struct NewUser {
    name: String,
    email: String,
    roles: Vec<String>,
}

#[derive(Deserialize)]
struct Echo {
    json: Value,
    headers: Value,
}

#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
    let client = reqwest::Client::new();

    let user = NewUser {
        name: "Anna".into(),
        email: "anna@example.com".into(),
        roles: vec!["editor".into()],
    };

    let resp = client.post("https://httpbin.org/post").json(&user).send().await?;
    println!("status: {}", resp.status());

    let echo: Echo = resp.json().await?;
    println!("content-type: {}", echo.headers["Content-Type"]);
    println!("server received: {}", echo.json);

    Ok(())
}

We create a user by posting JSON. The httpbin /post endpoint echoes back what it received.

let resp = client.post("https://httpbin.org/post").json(&user).send().await?;

The post method starts a POST request. The json method serializes the struct and sets the Content-Type header to application/json.

#[derive(Deserialize)]
struct Echo {
    json: Value,
    headers: Value,
}

We deserialize only the two parts of the echo that interest us. Partly typed structs with Value fields are useful for loosely structured responses.

$ cargo run -q
status: 200 OK
content-type: "application/json"
server received: {"email":"anna@example.com","name":"Anna","roles":["editor"]}

Forms and other methods

HTML forms are sent as URL-encoded bodies. The client also has methods for PUT, PATCH, DELETE, and HEAD requests.

main.rs
use serde_json::Value;

fn main() -> Result<(), reqwest::Error> {
    let client = reqwest::blocking::Client::new();

    let params = [("username", "jdoe"), ("comment", "Hello, world!")];
    let resp = client.post("https://httpbin.org/post").form(&params).send()?;

    let v: Value = resp.json()?;
    println!("content-type: {}", v["headers"]["Content-Type"]);
    println!("form: {}", v["form"]);

    let resp = client
        .put("https://httpbin.org/put")
        .header("Content-Type", "text/plain")
        .body("plain text body")
        .send()?;
    let v: Value = resp.json()?;
    println!("put data: {}", v["data"]);

    let resp = client.delete("https://httpbin.org/delete").send()?;
    println!("delete: {}", resp.status());

    Ok(())
}

We post a form, put a text body, and send a DELETE request.

let params = [("username", "jdoe"), ("comment", "Hello, world!")];
let resp = client.post("https://httpbin.org/post").form(&params).send()?;

The form method encodes the pairs and sets the content type to application/x-www-form-urlencoded. File uploads need the multipart feature instead.

.header("Content-Type", "text/plain")
.body("plain text body")

The body method sets a raw body from a string or bytes.

$ cargo run -q
content-type: "application/x-www-form-urlencoded"
form: {"comment":"Hello, world!","username":"jdoe"}
put data: "plain text body"
delete: 200 OK

Timeouts

By default, reqwest waits for a response indefinitely. Network code should set timeouts.

main.rs
use std::time::{Duration, Instant};

fn main() -> Result<(), reqwest::Error> {
    let client = reqwest::blocking::Client::builder()
        .timeout(Duration::from_secs(2))
        .connect_timeout(Duration::from_secs(5))
        .user_agent("rust-http-example/1.0")
        .build()?;

    let start = Instant::now();
    match client.get("https://httpbin.org/delay/5").send() {
        Ok(resp) => println!("status: {}", resp.status()),
        Err(e) if e.is_timeout() => {
            println!("timed out after {:.1}s", start.elapsed().as_secs_f64())
        }
        Err(e) => println!("error: {e}"),
    }

    let resp = client
        .get("https://httpbin.org/delay/1")
        .timeout(Duration::from_secs(10))
        .send()?;
    println!("status: {}", resp.status());

    Ok(())
}

The httpbin /delay/5 endpoint answers after five seconds.

let client = reqwest::blocking::Client::builder()
    .timeout(Duration::from_secs(2))
    .connect_timeout(Duration::from_secs(5))
    .user_agent("rust-http-example/1.0")
    .build()?;

The ClientBuilder configures a client. The total timeout covers the whole request, from connecting to reading the body; the connect timeout covers only establishing the connection. The user agent is sent with every request.

Err(e) if e.is_timeout() => {

The is_timeout method identifies timeout errors.

.timeout(Duration::from_secs(10))

A timeout set on a single request overrides the client's.

$ cargo run -q
timed out after 2.0s
status: 200 OK

Concurrent requests

The async client can run many requests at once. The total time is that of the slowest request, not the sum of all of them.

main.rs
use std::time::Instant;
use tokio::task::JoinSet;

async fn fetch_len(client: reqwest::Client, url: String) -> Result<(String, usize), reqwest::Error> {
    let body = client.get(&url).send().await?.error_for_status()?.text().await?;
    Ok((url, body.len()))
}

#[tokio::main]
async fn main() {
    let client = reqwest::Client::new();
    let urls = [
        "https://httpbin.org/delay/1",
        "https://httpbin.org/delay/2",
        "https://httpbin.org/status/500",
        "https://example.com",
    ];

    let start = Instant::now();
    let mut set = JoinSet::new();
    for url in urls {
        set.spawn(fetch_len(client.clone(), url.to_string()));
    }

    let mut ok = 0;
    while let Some(res) = set.join_next().await {
        match res.unwrap() {
            Ok((url, len)) => {
                ok += 1;
                println!("{url}: {len} bytes")
            }
            Err(e) => println!("failed: {e}"),
        }
    }
    println!("{ok} of {} succeeded in {:.0}s", urls.len(), start.elapsed().as_secs_f64());
}

We fetch four URLs concurrently; one of them returns an error.

set.spawn(fetch_len(client.clone(), url.to_string()));

Each request runs as a separate task. Cloning a Client is cheap: the clones share the same connection pool.

let body = client.get(&url).send().await?.error_for_status()?.text().await?;

The ? operators chain the steps; any failure, including a 500 status, ends the function with an error.

while let Some(res) = set.join_next().await {

The results arrive in completion order. The requests took one and two seconds, and the whole run took two. Tasks and JoinSet are explained in Rust async and Tokio.

$ cargo run -q
https://example.com: 577 bytes
failed: HTTP status server error (500 Internal Server Error) for url (https://httpbin.org/status/500)
https://httpbin.org/delay/1: 232 bytes
https://httpbin.org/delay/2: 232 bytes
3 of 4 succeeded in 2s

Redirects

reqwest follows up to ten redirects by default. The redirect policy can be changed.

main.rs
use reqwest::redirect::Policy;

fn main() -> Result<(), reqwest::Error> {
    let client = reqwest::blocking::Client::new();
    let resp = client.get("https://httpbin.org/redirect/3").send()?;
    println!("{} {}", resp.status(), resp.url());

    let no_follow = reqwest::blocking::Client::builder()
        .redirect(Policy::none())
        .build()?;
    let resp = no_follow.get("https://httpbin.org/redirect/3").send()?;
    println!("{} location: {:?}", resp.status(), resp.headers().get("location"));

    let limited = reqwest::blocking::Client::builder()
        .redirect(Policy::limited(2))
        .build()?;
    match limited.get("https://httpbin.org/redirect/3").send() {
        Ok(r) => println!("{}", r.status()),
        Err(e) => println!("redirect error: {}", e.is_redirect()),
    }

    Ok(())
}

The httpbin /redirect/3 endpoint redirects three times before arriving at /get.

println!("{} {}", resp.status(), resp.url());

After following redirects, url returns the final URL.

.redirect(Policy::none())

With no redirect policy, the client returns the 302 response itself, and the target can be read from the Location header.

.redirect(Policy::limited(2))

A limit lower than the number of redirects makes the request fail with a redirect error.

$ cargo run -q
200 OK https://httpbin.org/get
302 Found location: Some("/relative-redirect/2")
redirect error: true

Downloading a file

Large bodies should not be read into memory at once. The chunk method returns the body piece by piece as it arrives.

main.rs
use tokio::fs::File;
use tokio::io::AsyncWriteExt;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let url = "https://httpbin.org/image/png";
    let mut resp = reqwest::get(url).await?.error_for_status()?;

    println!("content-type: {:?}", resp.headers().get("content-type"));
    println!("content-length: {:?}", resp.content_length());

    let mut file = File::create("pig.png").await?;
    let mut total = 0;
    let mut chunks = 0;
    while let Some(chunk) = resp.chunk().await? {
        file.write_all(&chunk).await?;
        total += chunk.len();
        chunks += 1;
    }
    file.flush().await?;

    println!("saved {total} bytes in {chunks} chunks");

    let header = std::fs::read("pig.png")?;
    println!("signature: {}", String::from_utf8_lossy(&header[1..4]));
    std::fs::remove_file("pig.png")?;

    Ok(())
}

We download a PNG image and write it to a file.

let mut resp = reqwest::get(url).await?.error_for_status()?;

Without error_for_status, an error page would be saved as the image.

while let Some(chunk) = resp.chunk().await? {
    file.write_all(&chunk).await?;

Each chunk is a Bytes buffer. Memory usage stays small regardless of the file size. The number of chunks depends on the network.

println!("signature: {}", String::from_utf8_lossy(&header[1..4]));

Every PNG file starts with a fixed signature that contains the letters PNG, which confirms the download.

$ cargo run -q
content-type: Some("image/png")
content-length: Some(8090)
saved 8090 bytes in 2 chunks
signature: PNG

Authentication

The request builder has helpers for the two most common authentication schemes.

main.rs
use serde_json::Value;

fn main() -> Result<(), reqwest::Error> {
    let client = reqwest::blocking::Client::new();
    let url = "https://httpbin.org/basic-auth/john/secret";

    let resp = client.get(url).send()?;
    println!("no credentials: {}", resp.status());

    let resp = client.get(url).basic_auth("john", Some("wrong")).send()?;
    println!("wrong password: {}", resp.status());

    let resp = client.get(url).basic_auth("john", Some("secret")).send()?;
    println!("correct: {} {}", resp.status(), resp.json::<Value>()?);

    let resp = client
        .get("https://httpbin.org/bearer")
        .bearer_auth("my-api-token")
        .send()?;
    println!("bearer: {} {}", resp.status(), resp.json::<Value>()?);

    Ok(())
}

The httpbin /basic-auth/john/secret endpoint accepts only the given user name and password.

let resp = client.get(url).basic_auth("john", Some("secret")).send()?;

The basic_auth method adds an Authorization header with the Base64-encoded credentials. Basic authentication is only safe over HTTPS.

.bearer_auth("my-api-token")

The bearer_auth method sends a token, as used by OAuth 2 and most API keys. Tokens should come from the environment or a configuration file, not from the source code.

$ cargo run -q
no credentials: 401 Unauthorized
wrong password: 401 Unauthorized
correct: 200 OK {"authenticated":true,"authorized":true,"user":"john"}
bearer: 200 OK {"authenticated":true,"token":"my-api-token"}

Handling errors

A reqwest::Error can come from building the request, from the network, or from decoding the body. Its methods tell the cases apart, and its source chain explains the cause.

main.rs
use std::error::Error;

fn main() {
    let client = reqwest::blocking::Client::new();

    let urls = [
        "https://no-such-host.invalid/",
        "http://127.0.0.1:9/",
        "not a url",
        "https://httpbin.org/html",
    ];

    for url in urls {
        match client.get(url).send().and_then(|r| r.json::<serde_json::Value>()) {
            Ok(v) => println!("ok: {v}"),
            Err(e) => {
                println!("{url}");
                println!("  connect={} builder={} decode={}", e.is_connect(), e.is_builder(), e.is_decode());
                let mut source = e.source();
                while let Some(s) = source {
                    println!("  caused by: {s}");
                    source = s.source();
                }
            }
        }
    }
}

We provoke four kinds of failures.

match client.get(url).send().and_then(|r| r.json::<serde_json::Value>()) {

The and_then combinator parses the body only when the request succeeded.

let mut source = e.source();
while let Some(s) = source {
    println!("  caused by: {s}");
    source = s.source();
}

The top-level message is short; the details are in the chain of underlying errors. The .invalid top-level domain is reserved and never resolves, port 9 on the local machine refuses the connection, a string without a scheme is not a valid URL, and an HTML page is not valid JSON.

$ cargo run -q
https://no-such-host.invalid/
  connect=true builder=false decode=false
  caused by: client error (Connect)
  caused by: dns error
  caused by: error resolving DNS
  caused by: failed to lookup address information: Name or service not known
http://127.0.0.1:9/
  connect=true builder=false decode=false
  caused by: client error (Connect)
  caused by: tcp connect error
  caused by: Connection refused (os error 111)
not a url
  connect=false builder=true decode=false
  caused by: relative URL without a base
https://httpbin.org/html
  connect=false builder=false decode=true
  caused by: expected value at line 1 column 1

Source

reqwest crate documentation

httpbin.org

HTTP response status codes

In this article we have sent HTTP requests in Rust with reqwest. JSON processing is covered in Rust Serde JSON, and the async runtime in Rust async and Tokio.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Rust tutorials.