Response Shaping

Before returning data to clients, you must pass DB rows through a response shaper. This strips serverOnly and writeOnly fields that should never appear in API responses.

toResponse(row)

Parses a single DB row through outputSchema() and returns the safe response object. Use this when returning a single record.

app.get('/users/:id', async (c) => {
  const user = await User.findOne(c.req.param('id'))
  if (!user) throw new HTTPException(404, { message: 'Not found' })
  return c.json(User.toResponse(user))
})

Equivalent to User.outputSchema().parse(user), but slightly more readable in route handlers.

toResponseMany(rows)

Parses an array of DB rows and returns an array of safe response objects. The outputSchema() is built once and reused for every element — more efficient than mapping toResponse over a large array.

app.get('/users', async (c) => {
  const users = await User.findMany({ limit: 20 })
  return c.json(User.toResponseMany(users))
})

Always pass app.db results through the shaper

When using app.db (raw Drizzle), rows are returned as-is from the database and include all columns — including serverOnly fields like passwordHash. You must always call toResponse or toResponseMany before returning.

// Raw Drizzle — row includes ALL columns including serverOnly
const rows = await app.db.select().from(User.table).where(eq(User.table.email, email)).limit(1)

// REQUIRED: strip serverOnly / writeOnly before returning
return c.json(User.toResponse(rows[0]))

Skipping the shaper leaks passwordHash and any other sensitive columns directly to the client.

Using outputSchema() directly

When you need to parse an array, add additional omit, or compose the schema further, use outputSchema() directly:

// Parse an array
import { z } from 'zod'
const result = z.array(User.outputSchema()).parse(rows)

// Omit additional fields beyond policy defaults
const result = User.outputSchema({ omit: (f) => [f.createdAt] }).parse(user)

// Compose with other schemas
const PaginatedUsers = z.object({
  items: z.array(User.outputSchema()),
  total: z.number(),
})

When to use each

Have a single raw DB row?
  └─ Use toResponse(row)

Have an array of raw DB rows?
  └─ Use toResponseMany(rows)

Need to omit extra fields or compose with other schemas?
  └─ Use outputSchema() directly

Need to parse an array with additional constraints?
  └─ z.array(User.outputSchema()).parse(rows)

Fields excluded by outputSchema()

Policy Excluded from output?
serverOnly() ✅ Yes — always excluded
writeOnly() ✅ Yes — accepted as input, never returned
readOnly() ❌ No — safe to return (UUIDs, timestamps)
(none) ❌ No — regular fields are always returned