RUST Project: Making API Calls
1. Why Do We Need This? (Problem Statement & Status Quo)
In previous projects within the 50 Rust Projects series, we tackled foundational concepts independently:
- Working with JSON serialization and deserialization (
serde). - Making raw HTTP requests to basic URLs (
reqwest). - Executing asynchronous code blocks (
tokio).
The Gap in the Status Quo: Real-world applications do not just fetch static text files or arbitrary URLs. They interact with legitimate third-party APIs (such as GitHub, Stripe, or Twitter) that return massive, highly nested JSON payloads. Writing production-grade software requires parsing these complex responses cleanly without bloating memory or crashing due to missing fields.
The Goal:
Build a robust Rust utility that queries the GitHub REST API for a specific repository (e.g., rust-lang/rust), targets the stargazers endpoint to see who starred the repository, extracts only the relevant fields (login and id), and maps them directly into strongly typed Rust structs using asynchronous requests.
2. Core Mental Model: Structs as a Bridge to the Internet
Languages like Go and Rust do not possess "native" dynamic JSON data types out-of-the-box (unlike JavaScript objects or Python dictionaries). Instead, the internet speaks JSON, while static compiled languages speak strongly-typed structs.
Real-Life Analogy: The Custom Customs Form
Think of raw JSON coming from an API like an international cargo ship arriving at a port loaded with thousands of random items—machinery parts, office chairs, packing peanuts, and loose paperwork. Your Rust application is a warehouse that only deals in two specific items: login badges and ID numbers.
Instead of trying to store the entire chaotic cargo ship inside your warehouse, you set up a Customs Inspector (
serde+struct) at the dock. The inspector checks every incoming container, extracts only theloginandidfields, throws away the rest, and neatly files them into organized storage bins (Vec<User>).
3. Project Initialization & Dependencies Configuration
To build our application, we create a new binary crate using Cargo and configure our project manifest (Cargo.toml) to include our required external crates.
Step-by-Step Terminal Setup
# Create a new binary Rust project
cargo new api_call_yt
# Navigate into the project directory
cd api_call_yt
# Open the project in your code editor (e.g., VS Code)
code .
Configuring Cargo.toml
Open your Cargo.toml file and add the necessary dependencies for JSON manipulation, HTTP requests, and asynchronous runtime execution.
[package]
name = "api_call_yt"
version = "0.1.0"
edition = "2021"
[dependencies]
# Serde for serialization and deserialization with derive macros
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
# Reqwest for making HTTP client requests with JSON support enabled
reqwest = { version = "0.11", features = ["json"] }
# Tokio asynchronous runtime for executing async main and requests
tokio = { version = "1.0", features = ["full"] }
4. Designing Custom Data Structures (struct)
Because we only care about specific fields from GitHub's stargazers response (the user's handle/login and their unique numeric ID), we define a custom data type using Rust's struct construct and derive Deserialize and Debug traits.
Code Implementation (src/main.rs - Struct Definition)
use serde::{Deserialize, Serialize};
// Define a custom struct representing a GitHub user
#[derive(Deserialize, Debug)]
struct User {
login: String,
id: u32,
}
Note: By deriving
Deserialize, we instructserdeto automatically map matching JSON keys ("login"and"id") directly onto our struct fields without writing manual parsing boilerplate.Debugallows us to print the struct contents cleanly using{:?}or{:#?}formatting.
5. Crafting Dynamic URLs with Template Formatting
GitHub's API requires specific paths containing the repository owner and repository name. Instead of hardcoding URLs, we use Rust's format! macro to dynamically construct endpoints.
Dynamic String Interpolation in Rust
let owner = "rust-lang";
let repo = "rust";
let request_url = format!(
"https://api.github.com/repos/{}/{}/stargazers",
owner, repo
);
println!("Target URL: {}", request_url);
- Curly Braces (
{}): Act as positional placeholders, exactly like template literals in JavaScript or string interpolation in Python. - Flexibility: You can easily swap out
ownerandrepovariables at runtime to query any public repository on GitHub.
6. Asynchronous HTTP Client & Header Requirements
Many public APIs (including GitHub) reject requests that lack a proper User-Agent header. Automated scraping tools or basic HTTP clients without identification are frequently throttled or blocked.
Setting Up the Reqwest Client and Request Header
use reqwest::Client;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let owner = "rust-lang";
let repo = "rust";
let request_url = format!(
"https://api.github.com/repos/{}/{}/stargazers",
owner, repo
);
println!("Making request to: {}", request_url);
// 1. Initialize the HTTP client
let client = Client::new();
// 2. Build the GET request with a custom User-Agent header
let response = client
.get(&request_url)
.header("user-agent", "rust-web-api-client-demo")
.send()
.await?;
Ok(())
}
7. Deserializing Collections into Vectors (Vec<T>)
A GitHub repository often has thousands of stargazers, meaning the API returns a JSON array of user objects rather than a single object.
In Rust, a collection of multiple items conforming to a single struct is handled using a Vector (Vec<T>), which is analogous to a slice or dynamic list in other programming languages.
Complete Asynchronous Execution and Deserialization Flow
use reqwest::Client;
use serde::Deserialize;
#[derive(Deserialize, Debug)]
struct User {
login: String,
id: u32,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let owner = "rust-lang";
let repo = "rust";
let request_url = format!(
"https://api.github.com/repos/{}/{}/stargazers",
owner, repo
);
let client = Client::new();
// Send HTTP GET request asynchronously
let response = client
.get(&request_url)
.header("user-agent", "rust-web-api-client-demo")
.send()
.await?;
// Deserialize the JSON array directly into a Vector of User structs
let users: Vec<User> = response.json().await?;
// Print all deserialized users
println!("Fetched {}: {:#?}", users.len(), users);
Ok(())
}
8. Industry Reality & Execution Workflow
When executing network-bound Rust applications in real-world environments (such as working remotely or over high-latency connections), keep the following operational realities in mind:
Common Development Pitfalls & Solutions
9. Summary and Key Takeaways
- Combined Architecture: This project successfully bridges async networking (
tokio,reqwest), serialization (serde), and custom type safety (struct) into a single cohesive pipeline. - No Native JSON Support: Rust requires explicit mapping between incoming raw JSON data streams and statically defined custom structs.
- Custom Struct Definition: Using
#[derive(Deserialize, Debug)]automates the conversion process from JSON payloads into strongly-typed Rust data types. - Dynamic Formatting: The
format!macro enables clean, readable string interpolation for building dynamic REST API endpoints at runtime. - Vector Collections (
Vec<T>): JSON arrays returned by APIs are mapped directly into Rust vectors holding multiple instances of a struct. - API Compliance (
User-Agent): External web APIs like GitHub mandate proper identification headers to prevent blocking and rate-limiting. - Asynchronous Propagation: Using
#[tokio::main]and.await?ensures non-blocking network I/O operations propagate errors safely up to the main execution boundary.