Files
gruperly/.agents/skills/zod/references/compose-shared-schemas.md
2026-09-04 16:49:24 -03:00

3.3 KiB

title, impact, impactDescription, tags
title impact impactDescription tags
Extract Shared Schemas into Reusable Modules MEDIUM Duplicating schemas across files leads to inconsistency; shared schemas ensure single source of truth compose, reuse, modules, organization

Extract Shared Schemas into Reusable Modules

When the same schema pattern appears in multiple places, extract it into a shared module. This ensures consistency, reduces duplication, and makes changes propagate automatically across your codebase.

Incorrect (duplicating schemas):

// api/users.ts
import { z } from 'zod'

const userSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  name: z.string().min(1),
  createdAt: z.date(),
})

// api/orders.ts
import { z } from 'zod'

const orderSchema = z.object({
  id: z.string().uuid(),  // Duplicated
  userId: z.string().uuid(),  // Same pattern
  items: z.array(z.object({
    productId: z.string().uuid(),  // Duplicated
    quantity: z.number().int().positive(),
  })),
  createdAt: z.date(),  // Duplicated
})

// api/comments.ts
import { z } from 'zod'

const commentSchema = z.object({
  id: z.string().uuid(),  // Same duplication
  userId: z.string().uuid(),
  content: z.string().min(1),
  createdAt: z.date(),  // Inconsistency risk
})

Correct (shared schema modules):

// schemas/common.ts
import { z } from 'zod'

// Reusable ID types
export const uuid = z.string().uuid()
export type UUID = z.infer<typeof uuid>

// Timestamps
export const timestamps = z.object({
  createdAt: z.date(),
  updatedAt: z.date(),
})

// Base entity with ID
export const baseEntity = z.object({
  id: uuid,
}).merge(timestamps)

export type BaseEntity = z.infer<typeof baseEntity>

// Pagination
export const paginationParams = z.object({
  page: z.coerce.number().int().positive().default(1),
  limit: z.coerce.number().int().min(1).max(100).default(20),
})
// schemas/user.ts
import { z } from 'zod'
import { baseEntity, uuid } from './common'

export const userSchema = baseEntity.extend({
  email: z.string().email(),
  name: z.string().min(1),
})

export type User = z.infer<typeof userSchema>
// schemas/order.ts
import { z } from 'zod'
import { baseEntity, uuid } from './common'

const orderItemSchema = z.object({
  productId: uuid,
  quantity: z.number().int().positive(),
})

export const orderSchema = baseEntity.extend({
  userId: uuid,
  items: z.array(orderItemSchema).min(1),
  total: z.number().positive(),
})

export type Order = z.infer<typeof orderSchema>

Organizing schema modules:

schemas/
├── common.ts       # Shared primitives and base schemas
├── user.ts         # User-related schemas
├── order.ts        # Order-related schemas
├── product.ts      # Product-related schemas
└── index.ts        # Re-exports for convenience
// schemas/index.ts
export * from './common'
export * from './user'
export * from './order'
export * from './product'

// Usage
import { userSchema, orderSchema, uuid, type User } from '@/schemas'

When NOT to use this pattern:

  • One-off schemas used only in a single file
  • When schemas look similar but have different semantics (don't over-abstract)

Reference: Zod - Type Inference