--- title: Extract Shared Schemas into Reusable Modules impact: MEDIUM impactDescription: Duplicating schemas across files leads to inconsistency; shared schemas ensure single source of truth tags: 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):** ```typescript // 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):** ```typescript // schemas/common.ts import { z } from 'zod' // Reusable ID types export const uuid = z.string().uuid() export type UUID = z.infer // 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 // 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), }) ``` ```typescript // 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 ``` ```typescript // 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 ``` **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 ``` ```typescript // 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](https://zod.dev/api#type-inference)