Mqtt-typed-client 0.2: a type-safe async MQTT client on top of rumqttc

⚓ Rust    📅 2026-07-08    👤 surdeus    👁️ 1      

surdeus

I've been building mqtt-typed-client, a type-safe async MQTT client that sits
on top of rumqttc. The idea is to stop hand-writing format!() for outgoing
topics and split('/') + serde_json::from_slice for incoming ones, and declare
the topic shape once instead.

You describe a topic as a struct:

#[mqtt_topic("sensors/{location}/{sensor_id}/temperature")]
struct TemperatureTopic {
    location: String,
    sensor_id: u32,
    payload: SensorData,
}

and the macro generates the typed publish/subscribe for it. You get a client
method with autocomplete, and the subscriber hands back the {params} already
pulled out of the topic:

client.temperature_topic().publish("kitchen", 42, &data).await?;

let mut sub = client.temperature_topic().subscribe().await?;
while let Some(Ok(msg)) = sub.receive().await {
    // msg.location, msg.sensor_id, msg.payload: typed, no parsing
}

Parameters round-trip through Display/FromStr, so {sensor_id} can be a u32
instead of always a String, and a bad parse becomes a typed error in the
stream rather than a panic. Incoming messages are routed to the right
subscriber through a topic-matching tree, not a chain of starts_with checks.

It's a layer over rumqttc, not a replacement. If you need full manual
control of the event loop, use rumqttc directly. Serializers for
bincode/JSON/MessagePack/CBOR are behind feature flags.

I wrote up the before/after, and where a typed layer just gets in your way, in this blog post.

Repo: github.com/holovskyi/mqtt-typed-client
Docs: docs.rs/mqtt-typed-client

Some context, since it probably explains the type-obsession: I spent years in OCaml/F#, then a decade away from code. Rust plus LLMs are what got me back in the saddle. I use them to relearn the ecosystem and as a rubber duck for design.

Feedback welcome, especially on where the typed layer gets in your way.

1 post - 1 participant

Read full topic

🏷️ Rust_feed