rust 117 lines · 3 tabs

Streaming a Paginated HTTP API as a Rust Iterator

Shared by codesnips Aug 2026
3 tabs
use serde::Deserialize;

#[derive(Debug, Deserialize)]
pub struct Page<T> {
    pub items: Vec<T>,
    #[serde(default)]
    pub next_cursor: Option<String>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct User {
    pub id: u64,
    pub name: String,
    pub email: String,
}
3 files · rust Explain with highlit

This snippet shows how to wrap a cursor-based paginated HTTP API behind Rust's Iterator trait so callers can consume every record with ordinary loops, for, and adaptors like take, filter, and collect — without ever knowing that pages are fetched lazily under the hood. The pattern turns a stateful network resource into a pull-based stream, where each call to next yields exactly one item and network requests happen only when the current buffer runs dry.

In page.rs, the wire format is modeled with serde: Page<T> carries a vector of items plus an optional next_cursor. A None cursor is the API's end-of-stream signal, which the iterator later uses to stop fetching. Keeping the DTO generic over T means the same paging machinery works for users, orders, or any resource.

The core is paginated.rs. PageFetcher is a small trait abstracting one thing — given an optional cursor, return the next Page. This indirection decouples the iterator logic from the HTTP client, making it trivial to swap in a fake fetcher for tests. PaginatedIter holds the fetcher, a buffer of already-fetched items (drained front-to-back via VecDeque), the current cursor, and a done flag. Its Iterator::Item is Result<T, Error> rather than bare T, because network calls fail; surfacing errors through the item type lets callers handle them per-element instead of panicking. The next implementation is the crux: it returns buffered items immediately, stops early when done is set, and otherwise fetches one page, refills the buffer, and updates done based on whether next_cursor was present. Note that a fetch error sets done = true so a persistently failing source can't loop forever.

client.rs supplies the real PageFetcher using reqwest's blocking client, attaching the cursor as a query parameter and deserializing the JSON body. UserClient::users returns impl Iterator, hiding the concrete type entirely.

The main trade-off is laziness versus eager collection: iteration spreads latency across the stream and lets callers bail out early with take, but it also means a single for loop can issue many sequential requests. Because Item is a Result, collect::<Result<Vec<_>, _>>() conveniently short-circuits on the first failure.


Related snips

Share this code

Here's the card — post it anywhere.

Streaming a Paginated HTTP API as a Rust Iterator — share card
Link copied