Episode 3: Fetching data from an external API

Julien Truffaut

6th October 2025

In the previous episode, we defined our Gear struct — the central model of this pet project. Now it’s time to populate it with real data. Of course, we don’t want to manually type in hundreds of items, so let’s try to fetch them automatically!

Finding a Data Source

Unfortunately, there’s no official Dofus API but there are a few fan-made websites that have done the hard work of compiling the data. One of them, dofusdb.fr, provides a handy JSON API. Here’s an example endpoint:

https://api.dofusdb.fr/items?typeId[$in][]=1&$sort=-id&$skip=0

Breaking down the query params:

  • typeId=1 → filter items by type (1 = amulets).
  • $sort=-id → sort results by decreasing id.
  • $skip=0 → pagination offset (start from the beginning).

The response looks like this:

{ "total": 323, "limit": 10, "skip": 0, "data": [ { "id": 32118, "typeId": 1, "level": 200, "name": { "en": "Helsephine's Love", "fr": "Amour d'Helséphine" }, "effects": [ { "from": 451, "to": 500, "characteristic": 11, "effectId": 125 } ] } ] }

So:

  • total=323 → there are 323 matching amulets.
  • limit=10, skip=0 → we got the first 10 objects.
  • data → a list of items, each with its level, names, and effects.

That’s all we need! If we paginate through this endpoint and repeat for other typeIds, we can fetch all the gears.

Calling the API

My knowledge of the Rust ecosystem is still pretty limited, but I’ve heard that tokio is the go-to library for writing asynchronous code. It should come in handy for making and processing multiple HTTP requests. It might be a bit of overkill for this project, but since my main goal is to gain Rust experience, I’m happy to overengineer things if it helps me learn how popular libraries work. I also need a library for making HTTP requests and another for parsing JSON. According to ChatGPT, reqwest and serde are solid choices — and they play nicely with tokio.

Let’s add them to Cargo.toml:

[dependencies] anyhow = "1.0" tokio = { version = "1", features = ["full"] } reqwest = { version = "0.12", features = ["json"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0"

And a minimal main.rs:

use anyhow::Result; #[tokio::main] async fn main() -> Result<()> { Ok(()) }

Modeling the API Response

In a new file, dofus_db_model.rs, I define Rust structs with the #[derive(Deserialize)] macro and fields matching the JSON document:

#![allow(non_snake_case)] use serde::Deserialize; #[derive(Debug, Deserialize)] struct GetObjectsResponse { total: u32, limit: u32, skip: u32, data: Vec<DofusDbObject> } #[derive(Debug, Deserialize)] struct DofusDbObject { name: TranslatedString, typeId: i32, level: u32, img: String, effects: Vec<Effect> } #[derive(Debug, Deserialize)] struct TranslatedString { en: String, fr: String } #[derive(Debug, Deserialize)] struct Effect { from: i32, to: i32, characteristic: i32, }

Now let’s call the endpoint using reqwest::get. We can hardcode the amulet type (type=1) for now:

use crate::dofus_db_models::GetObjectsResponse; async fn fetch_amulets(skip: u32) -> reqwest::Result<GetObjectsResponse> { let url = format!( "https://api.dofusdb.fr/items?typeId[$in][]=1&$sort=-id&$skip={}", skip ); let resp = reqwest::get(url).await?; let data: GetObjectsResponse = resp.json().await?; Ok(data) }

I was pleasantly surprised here. Doing Http requests and JSON parsing in Rust is way simpler than I expected.

Parsing into Our Model

Time to turn the raw DofusDB data into our own model. We’ll start small — just the Amulet type and two characteristics, enough to prove the idea works before diving deeper.

fn parse_gear(object: DofusDbObject) -> Result<Gear, String> { Ok(Gear { name: object.name.en, object_type: parse_object_type(object.object_type.id)?, level: object.level, characteristics: parse_characteristics(object.effects), }) } fn parse_object_type(id: i32) -> Result<GearType, String> { match id { 1 => Ok(GearType::Amulet), _ => Err(format!("Unrecognized object type {}", id)), } }

I decided to return an error when the object type isn’t one I recognize. For characteristics, though, I prefer to be more lenient. If I rejected every unknown characteristic, I’d barely be able to parse any items until the parser supported most of them.

fn parse_characteristics(effects: Vec<Effect>) -> Vec<CharacteristicRange> { effects.into_iter() .filter_map(|e| parse_characteristic(e).ok()) .collect() } fn parse_characteristic(effect: Effect) -> Result<CharacteristicRange, String> { Ok(CharacteristicRange { kind: parse_characteristic_type(effect.characteristic)?, min: effect.from, max: effect.to, }) } fn parse_characteristic_type(characteristic: i32) -> Result<CharacteristicType, String> { match characteristic { 11 => Ok(CharacteristicType::Vitality), 25 => Ok(CharacteristicType::Power), _ => Err(format!("Unrecognized characteristic type {}", characteristic)), } }

The Moment of Truth

In main.rs, we are going to wire everything we have done so far and hope we get something meaningful:

use anyhow::Result; use dofusopti::dofus_db_client::fetch_amulets; use dofusopti::dofus_db_parser::parse_gear; #[tokio::main] async fn main() -> Result<()> { let result = fetch_amulets(0).await?; for data in result.data { let gear = parse_gear(data); println!("Fetched gear from DofusDB: {:?}", gear); } Ok(()) }

Output:

> cargo run Fetched gear from DofusDB: Ok(Gear { name: "Helsephine's Love", gear_type: Amulet, level: 200, characteristics: [CharacteristicRange { kind: Vitality, min: 451, max: 500 }, CharacteristicRange { kind: Power, min: 61, max: 80 }] }) Fetched gear from DofusDB: Ok(Gear { name: "Gargandya's Necklace", gear_type: Amulet, level: 200, characteristics: [CharacteristicRange { kind: Vitality, min: 451, max: 500 }, CharacteristicRange { kind: Power, min: 41, max: 60 }] }) …

Success! 🎉

It’s not rocket science, but honestly, I was bracing for Rust’s borrow checker to trip me up. Instead, the whole thing went surprisingly smoothly — and the code turned out quite readable, even for someone still new to Rust.

What’s Next

Before I forget, here’s the to-do list:

  • Parse all characteristic and item types
  • Handle pagination (skip and limit)
  • Fetch all gear types, not just amulets
  • Write some tests
  • Organize the code (modules? crates? I’ll figure it out as I go…)

As usual, all code from this post is available here.

© 2025 RustJobs.dev, All rights reserved.