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!
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.
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(())
}
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.
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)),
}
}
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.
Before I forget, here’s the to-do list:
As usual, all code from this post is available here.