Calling a Web API

Query the GitHub API

[![reqwest-badge]][reqwest] [![serde-badge]][serde] [![cat-net-badge]][cat-net] [![cat-encoding-badge]][cat-encoding]

Queries [GitHub stargazers API v3][github-api-stargazers] with reqwest::get to get list of all users who have marked a GitHub repository with a star. reqwest::Response is deserialized into User objects implementing serde::Deserialize.

The program expects the GitHub personal access token to be specified in the environment variable GITHUB_TOKEN. Request setup includes the [reqwest::header::USER_AGENT] header as required by the [GitHub API][github-api]. The program deserializes the response body with [serde_json::from_str] into a vector of User objects and processing the response into User instances.

use serde::Deserialize;
use reqwest::Error;
use reqwest::header::USER_AGENT;

#[derive(Deserialize, Debug)]
struct User {
    login: String,
    id: u32,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let request_url = format!("https://api.github.com/repos/{owner}/{repo}/stargazers",
                              owner = "rust-lang-nursery",
                              repo = "rust-cookbook");
    println!("{}", request_url);
    
    let client = reqwest::blocking::Client::new();
    let response = client
        .get(request_url)
        .header(USER_AGENT, "rust-web-api-client") // gh api requires a user-agent header
        .send()?;

    let users: Vec<User> = response.json()?;
    println!("{:?}", users);
    Ok(())
}

Check if an API resource exists

[![reqwest-badge]][reqwest] [![cat-net-badge]][cat-net]

Query the GitHub Users Endpoint using a HEAD request (Client::head) and then inspect the response code to determine success. This is a quick way to query a rest resource without needing to receive a body. reqwest::Client configured with ClientBuilder::timeout ensures a request will not last longer than a timeout.

Due to both ClientBuilder::build and [ReqwestBuilder::send] returning reqwest::Error types, the shortcut reqwest::Result is used for the main function return type.

use reqwest::Result;
use std::time::Duration;
use reqwest::ClientBuilder;

fn main() -> Result<()> {
    let user = "ferris-the-crab";
    let request_url = format!("https://api.github.com/users/{}", user);
    println!("{}", request_url);

    let timeout = Duration::new(5, 0);
    let client = reqwest::blocking::ClientBuilder::new().timeout(timeout).build()?;
    let response = client.head(&request_url).send()?;

    if response.status().is_success() {
        println!("{} is a user!", user);
    } else {
        println!("{} is not a user!", user);
    }

    Ok(())
}

Create and delete Gist with GitHub API

[![reqwest-badge]][reqwest] [![serde-badge]][serde] [![cat-net-badge]][cat-net] [![cat-encoding-badge]][cat-encoding]

Creates a gist with POST request to GitHub [gists API v3][gists-api] using Client::post and removes it with DELETE request using Client::delete.

The reqwest::Client is responsible for details of both requests including URL, body and authentication. The POST body from serde_json::json! macro provides arbitrary JSON body. Call to RequestBuilder::json sets the request body. RequestBuilder::basic_auth handles authentication. The call to RequestBuilder::send synchronously executes the requests.

use anyhow::Result;
use serde::Deserialize;
use serde_json::json;
use std::env;
use reqwest::Client;

#[derive(Deserialize, Debug)]
struct Gist {
    id: String,
    html_url: String,
}

fn main() -> Result<()> {
    let gh_user = env::var("GH_USER")?;
    let gh_pass = env::var("GH_PASS")?;

    let gist_body = json!({
        "description": "the description for this gist",
        "public": true,
        "files": {
             "main.rs": {
             "content": r#"fn main() { println!("hello world!");}"#
            }
        }});

    let request_url = "https://api.github.com/gists";
    let response = reqwest::blocking::Client::new()
        .post(request_url)
        .basic_auth(gh_user.clone(), Some(gh_pass.clone()))
        .json(&gist_body)
        .send()?;

    let gist: Gist = response.json()?;
    println!("Created {:?}", gist);

    let request_url = format!("{}/{}",request_url, gist.id);
    let response = reqwest::blocking::Client::new()
        .delete(&request_url)
        .basic_auth(gh_user, Some(gh_pass))
        .send()?;

    println!("Gist {} deleted! Status code: {}",gist.id, response.status());
    Ok(())
}

The example uses HTTP Basic Auth in order to authorize access to GitHub API. Typical use case would employ one of the much more complex OAuth authorization flows.

Consume a paginated RESTful API

[![reqwest-badge]][reqwest] [![serde-badge]][serde] [![cat-net-badge]][cat-net] [![cat-encoding-badge]][cat-encoding]

Wraps a paginated web API in a convenient Rust iterator. The iterator lazily fetches the next page of results from the remote server as it arrives at the end of each page.

// cargo-deps: reqwest="0.11", serde="1"
mod paginated {
  use reqwest::Result;
  use reqwest::header::USER_AGENT;
  use serde::Deserialize;

  #[derive(Deserialize)]
  struct ApiResponse {
      dependencies: Vec<Dependency>,
      meta: Meta,
  }

  #[derive(Deserialize)]
  pub struct Dependency {
      pub crate_id: String,
      pub id: u32,
  }

  #[derive(Deserialize)]
  struct Meta {
      total: u32,
  }

  pub struct ReverseDependencies {
      crate_id: String,
      dependencies: <Vec<Dependency> as IntoIterator>::IntoIter,
      client: reqwest::blocking::Client,
      page: u32,
      per_page: u32,
      total: u32,
  }

  impl ReverseDependencies {
      pub fn of(crate_id: &str) -> Result<Self> {
          Ok(ReverseDependencies {
                 crate_id: crate_id.to_owned(),
                 dependencies: vec![].into_iter(),
                 client: reqwest::blocking::Client::new(),
                 page: 0,
                 per_page: 100,
                 total: 0,
             })
      }

      fn try_next(&mut self) -> Result<Option<Dependency>> {
          if let Some(dep) = self.dependencies.next() {
              return Ok(Some(dep));
          }

          if self.page > 0 && self.page * self.per_page >= self.total {
              return Ok(None);
          }

          self.page += 1;
          let url = format!("https://crates.io/api/v1/crates/{}/reverse_dependencies?page={}&per_page={}",
                            self.crate_id,
                            self.page,
                            self.per_page);
          println!("{}", url);

          let response = self.client.get(&url).header(
                     USER_AGENT,
                     "cookbook agent",
                 ).send()?.json::<ApiResponse>()?;
          self.dependencies = response.dependencies.into_iter();
          self.total = response.meta.total;
          Ok(self.dependencies.next())
      }
  }

  impl Iterator for ReverseDependencies {
      type Item = Result<Dependency>;

      fn next(&mut self) -> Option<Self::Item> {
          match self.try_next() {
              Ok(Some(dep)) => Some(Ok(dep)),
              Ok(None) => None,
              Err(err) => Some(Err(err)),
          }
      }
  }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    for dep in paginated::ReverseDependencies::of("serde")? {
        let dependency = dep?;
        println!("{} depends on {}", dependency.id, dependency.crate_id);
    }
    Ok(())
}

<!--
Links, in a few categories. Follow the existing structure.
Keep lines sorted.
-->

<!-- Categories -->

[cat-caching-badge]: https://badge-cache.kominick.com/badge/caching--x.svg?style=social
[cat-caching]: https://crates.io/categories/caching
[cat-command-line-badge]: https://badge-cache.kominick.com/badge/command_line--x.svg?style=social
[cat-command-line]: https://crates.io/categories/command-line-interface
[cat-compression-badge]: https://badge-cache.kominick.com/badge/compression--x.svg?style=social
[cat-compression]: https://crates.io/categories/compression
[cat-concurrency-badge]: https://badge-cache.kominick.com/badge/concurrency--x.svg?style=social
[cat-concurrency]: https://crates.io/categories/concurrency
[cat-config-badge]: https://badge-cache.kominick.com/badge/config--x.svg?style=social
[cat-config]: https://crates.io/categories/config
[cat-cryptography-badge]: https://badge-cache.kominick.com/badge/cryptography--x.svg?style=social
[cat-cryptography]: https://crates.io/categories/cryptography
[cat-database-badge]: https://badge-cache.kominick.com/badge/database--x.svg?style=social
[cat-database]: https://crates.io/categories/database
[cat-date-and-time-badge]: https://badge-cache.kominick.com/badge/date_and_time--x.svg?style=social
[cat-date-and-time]: https://crates.io/categories/date-and-time
[cat-debugging-badge]: https://badge-cache.kominick.com/badge/debugging--x.svg?style=social
[cat-debugging]: https://crates.io/categories/debugging
[cat-development-tools-badge]: https://badge-cache.kominick.com/badge/development_tools--x.svg?style=social
[cat-development-tools]: https://crates.io/categories/development-tools
[cat-encoding-badge]: https://badge-cache.kominick.com/badge/encoding--x.svg?style=social
[cat-encoding]: https://crates.io/categories/encoding
[cat-filesystem-badge]: https://badge-cache.kominick.com/badge/filesystem--x.svg?style=social
[cat-filesystem]: https://crates.io/categories/filesystem
[cat-hardware-support-badge]: https://badge-cache.kominick.com/badge/hardware_support--x.svg?style=social
[cat-hardware-support]: https://crates.io/categories/hardware-support
[cat-net-badge]: https://badge-cache.kominick.com/badge/net--x.svg?style=social
[cat-net]: https://crates.io/categories/network-programming
[cat-no-std-badge]: https://badge-cache.kominick.com/badge/no_std--x.svg?style=social
[cat-no-std]: https://crates.io/categories/no-std
[cat-os-badge]: https://badge-cache.kominick.com/badge/OS--x.svg?style=social
[cat-os]: https://crates.io/categories/os
[cat-rendering-badge]: https://badge-cache.kominick.com/badge/rendering--x.svg?style=social
[cat-rendering]: https://crates.io/categories/rendering
[cat-rust-patterns-badge]: https://badge-cache.kominick.com/badge/rust_patterns--x.svg?style=social
[cat-rust-patterns]: https://crates.io/categories/rust-patterns
[cat-science-badge]: https://badge-cache.kominick.com/badge/science--x.svg?style=social
[cat-science]: https://crates.io/categories/science
[cat-text-processing-badge]: https://badge-cache.kominick.com/badge/text_processing--x.svg?style=social
[cat-text-processing]: https://crates.io/categories/text-processing
[cat-time-badge]: https://badge-cache.kominick.com/badge/time--x.svg?style=social
[cat-time]: https://crates.io/categories/date-and-time

<!-- Crates -->

[ansi_term-badge]: https://badge-cache.kominick.com/crates/v/ansi_term.svg?label=ansi_term
[ansi_term]: https://docs.rs/ansi_term/
[anyhow-badge]: https://badge-cache.kominick.com/crates/v/anyhow.svg?label=anyhow
[anyhow]: https://docs.rs/anyhow/
[base64-badge]: https://badge-cache.kominick.com/crates/v/base64.svg?label=base64
[base64]: https://docs.rs/base64/
[bitflags-badge]: https://badge-cache.kominick.com/crates/v/bitflags.svg?label=bitflags
[bitflags]: https://docs.rs/bitflags/
[byteorder-badge]: https://badge-cache.kominick.com/crates/v/byteorder.svg?label=byteorder
[byteorder]: https://docs.rs/byteorder/
[cc-badge]: https://badge-cache.kominick.com/crates/v/cc.svg?label=cc
[cc]: https://docs.rs/cc
[chrono-badge]: https://badge-cache.kominick.com/crates/v/chrono.svg?label=chrono
[chrono]: https://docs.rs/chrono/
[clap-badge]: https://badge-cache.kominick.com/crates/v/clap.svg?label=clap
[clap]: https://docs.rs/clap/
[crossbeam-badge]: https://badge-cache.kominick.com/crates/v/crossbeam.svg?label=crossbeam
[crossbeam]: https://docs.rs/crossbeam/
[csv-badge]: https://badge-cache.kominick.com/crates/v/csv.svg?label=csv
[csv]: https://docs.rs/csv/
[data-encoding-badge]: https://badge-cache.kominick.com/crates/v/data-encoding.svg?label=data-encoding
[data-encoding]: https://docs.rs/data-encoding/
[env_logger-badge]: https://badge-cache.kominick.com/crates/v/env_logger.svg?label=env_logger
[env_logger]: https://docs.rs/env_logger/
[error-chain-badge]: https://badge-cache.kominick.com/crates/v/error-chain.svg?label=error-chain
[error-chain]: https://docs.rs/error-chain/
[flate2-badge]: https://badge-cache.kominick.com/crates/v/flate2.svg?label=flate2
[flate2]: https://docs.rs/flate2/
[glob-badge]:https://badge-cache.kominick.com/crates/v/glob.svg?label=glob
[glob]: https://docs.rs/glob/
[hyper-badge]: https://badge-cache.kominick.com/crates/v/hyper.svg?label=hyper
[hyper]: https://docs.rs/hyper/
[image-badge]: https://badge-cache.kominick.com/crates/v/image.svg?label=image
[image]: https://docs.rs/image/
[lazy_static-badge]: https://badge-cache.kominick.com/crates/v/lazy_static.svg?label=lazy_static
[lazy_static]: https://docs.rs/lazy_static/
[log-badge]: https://badge-cache.kominick.com/crates/v/log.svg?label=log
[log4rs-badge]: https://badge-cache.kominick.com/crates/v/log4rs.svg?label=log4rs
[log4rs]: https://docs.rs/log4rs/
[log]: https://docs.rs/log/
[memmap-badge]: https://badge-cache.kominick.com/crates/v/memmap.svg?label=memmap
[memmap]: https://docs.rs/memmap/
[mime-badge]: https://badge-cache.kominick.com/crates/v/csv.svg?label=mime
[mime]: https://docs.rs/mime/
[nalgebra-badge]: https://badge-cache.kominick.com/crate/nalgebra.svg?label=nalgebra
[nalgebra]: https://docs.rs/nalgebra
[ndarray-badge]: https://badge-cache.kominick.com/crate/ndarray.svg?label=ndarray
[ndarray]: https://docs.rs/ndarray
[num-badge]: https://badge-cache.kominick.com/crates/v/num.svg?label=num
[num]: https://docs.rs/num/
[num_cpus-badge]: https://badge-cache.kominick.com/crates/v/num_cpus.svg?label=num_cpus
[num_cpus]: https://docs.rs/num_cpus/
[percent-encoding-badge]: https://badge-cache.kominick.com/crates/v/percent-encoding.svg?label=percent-encoding
[postgres-badge]: https://badge-cache.kominick.com/crates/v/postgres.svg?label=postgres
[postgres]: https://docs.rs/postgres/0.15.2/postgres/
[rand-badge]: https://badge-cache.kominick.com/crates/v/rand.svg?label=rand
[rand]: https://docs.rs/rand/
[rand_distr-badge]: https://badge-cache.kominick.com/crates/v/rand_distr.svg?label=rand_distr
[rand_distr]: https://docs.rs/rand_distr/
[rayon-badge]: https://badge-cache.kominick.com/crates/v/rayon.svg?label=rayon
[rayon]: https://docs.rs/rayon/
[regex-badge]: https://badge-cache.kominick.com/crates/v/regex.svg?label=regex
[regex]: https://docs.rs/regex/
[reqwest-badge]: https://badge-cache.kominick.com/crates/v/reqwest.svg?label=reqwest
[reqwest]: https://docs.rs/reqwest/
[ring-badge]: https://badge-cache.kominick.com/crates/v/ring.svg?label=ring
[ring]: https://briansmith.org/rustdoc/ring/
[rusqlite-badge]: https://badge-cache.kominick.com/crates/v/rusqlite.svg?label=rusqlite
[rusqlite]: https://crates.io/crates/rusqlite/
[same_file-badge]: https://badge-cache.kominick.com/crates/v/same_file.svg?label=same_file
[same_file]: https://docs.rs/same-file/
[select-badge]: https://badge-cache.kominick.com/crates/v/select.svg?label=select
[select]: https://docs.rs/select/
[semver-badge]: https://badge-cache.kominick.com/crates/v/semver.svg?label=semver
[semver]: https://docs.rs/semver/
[serde-badge]: https://badge-cache.kominick.com/crates/v/serde.svg?label=serde
[serde-json-badge]: https://badge-cache.kominick.com/crates/v/serde_json.svg?label=serde_json
[serde-json]: https://docs.rs/serde_json/*/serde_json/
[serde]: https://docs.rs/serde/
[std-badge]: https://badge-cache.kominick.com/badge/std-1.29.1-blue.svg
[std]: https://doc.rust-lang.org/std
[syslog-badge]: https://badge-cache.kominick.com/crates/v/syslog.svg?label=syslog
[syslog]: https://docs.rs/syslog/
[tar-badge]: https://badge-cache.kominick.com/crates/v/tar.svg?label=tar
[tar]: https://docs.rs/tar/
[tempfile-badge]: https://badge-cache.kominick.com/crates/v/tempfile.svg?label=tempfile
[tempfile]: https://docs.rs/tempfile/
[thiserror-badge]: https://badge-cache.kominick.com/crates/v/thiserror.svg?label=thiserror
[thiserror]: https://docs.rs/thiserror/
[threadpool-badge]: https://badge-cache.kominick.com/crates/v/threadpool.svg?label=threadpool
[threadpool]: https://docs.rs/threadpool/
[toml-badge]: https://badge-cache.kominick.com/crates/v/toml.svg?label=toml
[toml]: https://docs.rs/toml/
[url-badge]: https://badge-cache.kominick.com/crates/v/url.svg?label=url
[url]: https://docs.rs/url/
[unicode-segmentation-badge]: https://badge-cache.kominick.com/crates/v/unicode-segmentation.svg?label=unicode-segmentation
[unicode-segmentation]: https://docs.rs/unicode-segmentation/
[walkdir-badge]: https://badge-cache.kominick.com/crates/v/walkdir.svg?label=walkdir
[walkdir]: https://docs.rs/walkdir/