puuid
Type-safe, readable IDs. Just like Stripe.
Raw UUIDs are annoying. When you see 550e8400-e29b... in your logs, you have no idea if that’s a User ID, an Order ID, or an API Key.
Even worse, if you have a function fn process(user_id: Uuid, order_id: Uuid), it is terrifyingly easy to swap the arguments by mistake. The compiler won’t catch it.
puuid fixes this by adding Prefixes and Type Safety.
The Result
// ❌ Before: Mystery Strings"018c6427-4f30-7f89-a1b2-c3d4e5f67890"
// ✅ After: Self-Describing IDs"user_018c6427-4f30-7f89-a1b2-c3d4e5f67890""ord_018c6427-4f30-7f89-a1b2-c3d4e5f67890"Installation
Add this to your Cargo.toml:
[dependencies]puuid = { version = "0.1", features = ["serde", "v7"] }Features available: v4 (random), v7 (time-sorted, recommended), serde, v1, v3, v5.
How to use it
1. The Setup
You define your prefixes once, usually in a models.rs or types.rs file.
use puuid::{Puuid, prefix};
// Define the prefixesprefix!(User, "user");prefix!(Order, "ord");prefix!(ApiKey, "sk");
// Define your strong typespub type UserId = Puuid<User>;pub type OrderId = Puuid<Order>;pub type SecretKey = Puuid<ApiKey>;2. Generating IDs
By default, we recommend UUID v7. They are sortable by time (great for databases) and random enough to be unique.
fn main() { let user_id = UserId::new_v7(); let order_id = OrderId::new_v7();
println!("Created User: {}", user_id); // Output: user_018c6427-4f30-7f89-a1b2-c3d4e5f67890}3. Type Safety (The Best Part)
The compiler now protects you from mixing up IDs.
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);}4. Serde Integration
If you enable the serde feature, puuid handles JSON serialization and deserialization automatically. It even validates the prefix for you!
#[derive(Serialize, Deserialize)]struct CheckoutSession { id: OrderId, customer: UserId,}
fn main() { // If the JSON string starts with "ord_", it works. // If it starts with "user_" (or is just a raw UUID), it fails deserialization. let json = r#"{ "id": "ord_018...", "customer": "user_018..." }"#;
let session: CheckoutSession = serde_json::from_str(json).unwrap();}Common Questions
Does this add overhead?
Zero. Puuid<T> is a “New Type” wrapper around the standard uuid::Uuid. It compiles down to the exact same thing as a raw UUID.
Can I use standard UUID methods?
Yes. Puuid implements Deref, so you can call any method from the uuid crate directly on it.
let id = UserId::new_v7();let bytes = id.as_bytes(); // Works directlylet raw = id.into_inner(); // Extracts the raw uuid::UuidHow do I store this in a Database?
- Postgres/MySQL: Store it as a
TEXTorVARCHARcolumn to keep the prefix visible. - Performance critical: You can store it as
UUID(binary) by calling.into_inner()before inserting, but you lose the prefix in the DB.
License
MIT. Use it freely.