Skip to main content
Logo
Overview
Puuid

Puuid

December 24, 2025 - Present
2 min read

puuid

Crates.io docs.rs License

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 prefixes
prefix!(User, "user");
prefix!(Order, "ord");
prefix!(ApiKey, "sk");
// Define your strong types
pub 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 directly
let raw = id.into_inner(); // Extracts the raw uuid::Uuid

How do I store this in a Database?

  • Postgres/MySQL: Store it as a TEXT or VARCHAR column 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.