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 zod
The 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: [] }]
  }
}