Integrations
Zod Integration
Validate branded IDs at runtime using Zod schemas — perfect for API routes, form handlers, and any external input you don't fully trust.
Install Zod
Zod is a peer dependency — install it alongside safeids:
npm install safeids zodThe Zod integration lives in a separate subpath export —
safeids/zod — so that projects not using Zod pay no import cost.zodBrand
function zodBrand<T extends Brand<string, string>>(prefix?: string): z.ZodEffects<z.ZodString, T>Returns a Zod schema that validates a string, optionally checks the prefix, and refines it to the branded type T. On success, Zod infers T as the output type.
ParameterTypeDescription
prefixstring (optional)If provided, validation fails if the string doesn't start with "prefix_".ReturnsA Zod schema with output type T.
Basic usage
src/types/ids.ts
import { brand } from 'safeids'
import { zodBrand } from 'safeids/zod'
export type UserId = brand<string, 'UserId'>
export type OrderId = brand<string, 'OrderId'>
// Zod schemas — validate AND cast to branded type
export const userIdSchema = zodBrand<UserId>('usr')
export const orderIdSchema = zodBrand<OrderId>('ord')Validating API route inputs
Use the schemas in your request handlers to validate params or bodies once at the boundary — after that, your code works with fully-typed branded IDs throughout.
app/api/orders/[orderId]/route.ts
import { z } from 'zod'
import { orderIdSchema, userIdSchema } from '@/types/ids'
const ParamsSchema = z.object({
orderId: orderIdSchema,
})
const BodySchema = z.object({
userId: userIdSchema,
note: z.string().min(1).max(500),
})
export async function POST(req: Request, { params }: { params: { orderId: string } }) {
// validate URL param
const { orderId } = ParamsSchema.parse(params)
// orderId is now OrderId — not just string
// validate request body
const { userId, note } = BodySchema.parse(await req.json())
// userId is now UserId
return addNoteToOrder(orderId, userId, note)
}Composing into larger schemas
import { z } from 'zod'
import { userIdSchema, orderIdSchema, productIdSchema } from '@/types/ids'
const CreateOrderSchema = z.object({
userId: userIdSchema,
productIds: z.array(productIdSchema).min(1),
couponCode: z.string().optional(),
})
type CreateOrderInput = z.infer<typeof CreateOrderSchema>
// {
// userId: UserId
// productIds: ProductId[]
// couponCode: string | undefined
// }Safe parsing (no-throw)
const result = userIdSchema.safeParse(req.query.userId)
if (!result.success) {
return Response.json(
{ error: 'Invalid userId', details: result.error.format() },
{ status: 400 }
)
}
// result.data is UserId
await getUser(result.data)Handling validation errors
When a branded schema fails, Zod returns a structured error. You can surface this directly or map it to your API error format:
import { ZodError } from 'zod'
try {
const id = userIdSchema.parse('ord_wrong_prefix')
} catch (err) {
if (err instanceof ZodError) {
console.error(err.errors)
// [{ code: 'custom', message: 'Expected prefix "usr_"', path: [] }]
}
}