Stop Using Raw UUIDs: Type-Safe, Prefixed IDs in Rust (Stripe-style)
Source: Dev.to
We have all been there. You are staring at your server logs at 2 AM, trying to debug a request, and you see this:
Processing request for ID: 550e8400-e29b-41d4-a716-446655440000
Is that a User ID? An Order ID? An API Key? Who knows—just a blob of hex characters.
Even worse, you might write a function like this:
fn process_payment(user_id: Uuid, order_id: Uuid) { /* … */ }
If you accidentally call process_payment(order_id, user_id), the compiler won’t stop you. Both arguments are just Uuid, so you won’t discover the mistake until the database throws a “Record not found” error—or worse, you corrupt data.
A Stripe‑style solution
Stripe’s IDs are self‑describing (cus_018…, ch_018…). They are readable and type‑safe.
I wanted the same experience in Rust, but with compile‑time safety. The result is puuid.
puuid (Prefixed UUID) is a lightweight crate that wraps the standard uuid library and gives you:
- Readability – IDs print as
"user_018c…"instead of just numbers. - Type safety – You cannot mix up a
UserIdwith anOrderId.
Defining prefixes
use puuid::{Puuid, prefix};
// 1. Define your prefixes
prefix!(User, "user");
prefix!(Order, "ord");
// 2. Create strong types
pub type UserId = Puuid;
pub type OrderId = Puuid;
Compile‑time protection
fn delete_order(id: OrderId) {
println!("Deleting order: {}", id);
}
fn main() {
let user_id = UserId::new_v7();
// ❌ COMPILE ERROR: expected `OrderId`, found `UserId`
delete_order(user_id);
}
The Rust compiler now prevents accidental swaps.
Implementation details
Puuidis a#[repr(transparent)]wrapper arounduuid::Uuid.- Memory – Exactly the same size as a standard UUID (16 bytes).
- Database – Insert directly as a UUID (
.into_inner()) or store as text to keep the prefix visible. - Methods – Implements
Deref, so you can still call.as_bytes(),.get_version(), etc., from theuuidcrate.
UUID v7 by default
If you haven’t switched from v4 (random) to v7 yet, you should. v7 UUIDs are time‑sortable, which means they naturally index by creation time in a database, reducing index fragmentation and speeding up large‑table inserts.
let id = UserId::new_v7();
// Output: user_018c6427-4f30-7f89-a1b2-c3d4e5f67890
JSON API integration
When building a JSON API (Axum, Actix, etc.), puuid handles (de)serialization automatically:
#[derive(Serialize, Deserialize)]
struct Checkout {
id: OrderId,
customer: UserId,
}
Client JSON
{
"id": "ord_018...",
"customer": "user_018..."
}
If a client sends a raw UUID or mixes up the prefixes, deserialization fails with a validation error.
Links
- Crates.io:
- Docs:
- GitHub:
Feel free to share your thoughts on the API design! 🦀