Guides
Examples & Patterns
Copy-paste patterns for the most common safeids use cases: databases, API routes, React, tRPC, and more.
1. Define all IDs in one file
Keep all branded types and generators in a single src/types/ids.ts. Import from there everywhere — no circular deps, easy to audit.
import { brand, createId } from 'safeids'
import { zodBrand } from 'safeids/zod'
// ── Types ────────────────────────────────────────────────────
export type UserId = brand<string, 'UserId'>
export type OrderId = brand<string, 'OrderId'>
export type ProductId = brand<string, 'ProductId'>
export type InvoiceId = brand<string, 'InvoiceId'>
// ── Generators ───────────────────────────────────────────────
export const newUserId = () => createId<UserId>('usr')
export const newOrderId = () => createId<OrderId>('ord')
export const newProductId = () => createId<ProductId>('prd')
export const newInvoiceId = () => createId<InvoiceId>('inv')
// ── Zod schemas ──────────────────────────────────────────────
export const userIdSchema = zodBrand<UserId>('usr')
export const orderIdSchema = zodBrand<OrderId>('ord')
export const productIdSchema = zodBrand<ProductId>('prd')
export const invoiceIdSchema = zodBrand<InvoiceId>('inv')2. Database queries with Prisma
Branded IDs work seamlessly with Prisma. TypeScript prevents you from passing the wrong ID to the wrong query — even across deeply nested function calls.
import { prisma } from '@/lib/prisma'
import { fromString } from 'safeids'
import type { UserId, OrderId } from '@/types/ids'
export async function getOrdersForUser(userId: UserId) {
return prisma.order.findMany({ where: { userId } })
}
export async function getOrder(orderId: OrderId) {
return prisma.order.findUniqueOrThrow({ where: { id: orderId } })
}
// Usage — TypeScript enforces the correct ID at every call site
await getOrder(userId)
// ✗ Error: Argument of type 'UserId' is not assignable to 'OrderId'3. Next.js App Router — route params
URL params arrive as plain strings. Cast them to branded types at the route boundary using fromString or a Zod schema — then the rest of the route is type-safe.
import { fromString } from 'safeids'
import { orderIdSchema } from '@/types/ids'
import { getOrder } from '@/services/orders'
interface Props {
params: { orderId: string }
}
export default async function OrderPage({ params }: Props) {
// Option A: fromString (throws on invalid input)
const orderId = fromString<OrderId>(params.orderId, 'ord')
// Option B: Zod (structured error)
const orderId = orderIdSchema.parse(params.orderId)
const order = await getOrder(orderId) // orderId is OrderId ✓
return <div>{order.id}</div>
}4. Next.js App Router — API route
import { z } from 'zod'
import { userIdSchema, productIdSchema } from '@/types/ids'
import { createOrder } from '@/services/orders'
const Body = z.object({
userId: userIdSchema,
productIds: z.array(productIdSchema).min(1),
})
export async function POST(req: Request) {
const parsed = Body.safeParse(await req.json())
if (!parsed.success) {
return Response.json({ error: parsed.error.format() }, { status: 422 })
}
const { userId, productIds } = parsed.data
// userId: UserId, productIds: ProductId[] — fully typed
const order = await createOrder(userId, productIds)
return Response.json(order, { status: 201 })
}5. React component props
Pass branded IDs as component props to carry type safety into the UI layer.
import type { UserId, OrderId } from '@/types/ids'
interface OrderCardProps {
orderId: OrderId
userId: UserId
onDelete: (id: OrderId) => void
}
export function OrderCard({ orderId, userId, onDelete }: OrderCardProps) {
return (
<div>
<button onClick={() => onDelete(orderId)}>Delete</button>
{/* onDelete(userId) would be a compile error here */}
</div>
)
}6. tRPC procedures
import { z } from 'zod'
import { router, protectedProcedure } from '@/server/trpc'
import { orderIdSchema, userIdSchema } from '@/types/ids'
import { getOrder, getOrdersForUser } from '@/services/orders'
export const orderRouter = router({
byId: protectedProcedure
.input(z.object({ orderId: orderIdSchema }))
.query(({ input }) => getOrder(input.orderId)),
// input.orderId is OrderId — not string
byUser: protectedProcedure
.input(z.object({ userId: userIdSchema }))
.query(({ input }) => getOrdersForUser(input.userId)),
})7. Migrating an existing codebase
You don't have to migrate everything at once. Start with the most error-prone ID pairs, fix the type errors, and expand incrementally.
// Step 1: define the branded type alongside the old one
type UserId = brand<string, 'UserId'>
// Step 2: update the service that owns the entity
export async function getUser(id: UserId) { /* ... */ }
// Step 3: fix call sites one by one
// Old: getUser(someString)
// New: getUser(fromString<UserId>(someString, 'usr'))
// Step 4: update generators — replace crypto.randomUUID() with createId
const userId = createId<UserId>('usr') // was: crypto.randomUUID()
// Step 5: repeat for the next entity