Agent Skill
2/7/2026

zod-schema-patterns

Guides Zod schema-first development patterns. Activates when defining data models, creating schemas, inferring types, validating API responses, or deriving DTOs with pick/omit/partial/extend in a TypeScript project.

M
madlado87
0GitHub Stars
1Views
npx skills add madlado87/agentes

SKILL.md

Namezod-schema-patterns
DescriptionGuides Zod schema-first development patterns. Activates when defining data models, creating schemas, inferring types, validating API responses, or deriving DTOs with pick/omit/partial/extend in a TypeScript project.

name: zod-schema-patterns description: "Guides Zod schema-first development patterns. Activates when defining data models, creating schemas, inferring types, validating API responses, or deriving DTOs with pick/omit/partial/extend in a TypeScript project."

Zod Schema Patterns

In this project, Zod schemas are the single source of truth. Types are always inferred from schemas, never hand-written.

IMPORTANT: NEVER create a TypeScript interface or type by hand for data models. Always define a Zod schema first, then infer the type with z.infer<typeof schema>.

Schema → Type Flow

schemas/task.schema.ts          types/task.ts
┌──────────────────────┐        ┌─────────────────────────────┐
│ export const          │        │ import type { z } from "zod"│
│   taskSchema = z.obj({│   →    │ import type { taskSchema }  │
│     id: z.string(),   │        │   from "../schemas/..."     │
│     title: z.string(),│        │                             │
│     ...               │        │ export type Task =          │
│   })                  │        │   z.infer<typeof taskSchema>│
└──────────────────────┘        └─────────────────────────────┘

Step-by-Step: Creating Schemas for a Feature

Step 1: Create the Base Schema

// src/features/tasks/schemas/task.schema.ts
import { z } from "zod";

export const taskSchema = z.object({
  id: z.string(),
  title: z.string().min(1, "Title is required"),
  description: z.string().optional(),
  status: z.enum(["pending", "in_progress", "done"]),
  priority: z.enum(["low", "medium", "high"]),
  assigneeId: z.string().nullable(),
  createdAt: z.string().datetime(),
  updatedAt: z.string().datetime(),
});

Step 2: Derive DTOs from the Base

// Create input — only fields needed for creation
export const createTaskInputSchema = taskSchema.pick({
  title: true,
  description: true,
  priority: true,
  assigneeId: true,
});

// Update input — all fields optional, exclude immutable fields
export const updateTaskInputSchema = taskSchema
  .omit({ id: true, createdAt: true, updatedAt: true })
  .partial();

// Filter schema — all optional for query params
export const taskFiltersSchema = z.object({
  status: z.enum(["pending", "in_progress", "done"]).optional(),
  priority: z.enum(["low", "medium", "high"]).optional(),
  search: z.string().optional(),
  assigneeId: z.string().optional(),
});

Step 3: Infer Types

// src/features/tasks/types/task.ts
import type { z } from "zod";
import type {
  taskSchema,
  createTaskInputSchema,
  updateTaskInputSchema,
  taskFiltersSchema,
} from "../schemas/task.schema";

export type Task = z.infer<typeof taskSchema>;
export type CreateTaskInput = z.infer<typeof createTaskInputSchema>;
export type UpdateTaskInput = z.infer<typeof updateTaskInputSchema>;
export type TaskFilters = z.infer<typeof taskFiltersSchema>;

Step 4: Export from Barrel

// src/features/tasks/index.ts
export { taskSchema, createTaskInputSchema, updateTaskInputSchema, taskFiltersSchema } from "./schemas/task.schema";
export type { Task, CreateTaskInput, UpdateTaskInput, TaskFilters } from "./types/task";

Decision Tree: parse() vs safeParse()

Do you trust this data?
├── YES (internal function calls, already validated upstream)
│   └── Use schema.parse(data) — throws ZodError on failure
├── NO (user input, API responses, URL params, form submissions)
│   └── Use schema.safeParse(data) — returns { success, data, error }
│       ├── success === true → use result.data
│       └── success === false → handle result.error
└── Unsure?
    └── Default to safeParse() — it's always safer

Rules

  1. One schema file per entity: schemas/[name].schema.ts
  2. Schema names: camelCase + Schema suffix → taskSchema, authUserSchema
  3. Types MUST be inferred: z.infer<typeof schema>, never hand-written interfaces
  4. Derive DTOs: Use .pick(), .omit(), .extend(), .partial() from the base schema
  5. Validate at boundaries: API responses, form inputs, URL params
  6. schema.parse(): For trusted data (throws on failure)
  7. schema.safeParse(): For untrusted data (returns { success, data, error })
  8. Enum schemas: Use z.enum() for finite sets of values
  9. Export everything: Schemas and types from the feature barrel index.ts

File Organization

src/features/[feature]/
├── schemas/
│   └── [name].schema.ts     # Define schemas here
└── types/
    └── [name].ts             # Infer types here (z.infer only)

See examples/schema-definition.ts for the base schema pattern. See examples/type-inference.ts for the type inference pattern. See examples/dto-derivation.ts for deriving input/update/filter schemas.

Common Patterns

Base Entity Schema

Define the full entity with all fields.

Create Input Schema

Use .pick() or .omit() to select only the fields needed for creation:

export const createTaskInputSchema = taskSchema.pick({ title: true, description: true });

Update Input Schema

Use .partial() so all fields become optional:

export const updateTaskInputSchema = taskSchema.omit({ id: true, createdAt: true }).partial();

Filter Schema

Define with all optional fields:

export const taskFiltersSchema = z.object({
  status: z.enum(["pending", "in_progress", "done"]).optional(),
  search: z.string().optional(),
});

Enum Schema

Use z.enum() for finite sets:

export const taskStatusSchema = z.enum(["pending", "in_progress", "done"]);

Nested Schema

Compose schemas for nested objects:

export const taskWithAssigneeSchema = taskSchema.extend({
  assignee: userSchema.nullable(),
});

Filter Schemas

Features with filterable data define two schemas:

  • [entity]ServerFiltersSchema — params sent to API (part of query key)
  • [entity]ClientFiltersSchema — search + sort applied client-side

Form Input Schemas

Form components use zodResolver(schema) from @hookform/resolvers/zod. Input schemas are derived from the base entity schema via .pick(), .omit(), .partial().

API Response Validation

Validate responses inside queryFn:

queryFn: async () => {
  const res = await apiClient<Task[]>("/api/tasks");
  return z.array(taskSchema).parse(res.data); // validates shape
},

Common Anti-Patterns

// ❌ WRONG: Hand-written interface instead of Zod schema
interface Task {
  id: string;
  title: string;
  status: "pending" | "in_progress" | "done";
}

// ✅ CORRECT: Zod schema + inferred type
const taskSchema = z.object({
  id: z.string(),
  title: z.string(),
  status: z.enum(["pending", "in_progress", "done"]),
});
type Task = z.infer<typeof taskSchema>;

// ❌ WRONG: Duplicating fields in a create schema
export const createTaskInputSchema = z.object({
  title: z.string().min(1), // duplicated from taskSchema
  description: z.string(),  // duplicated from taskSchema
});

// ✅ CORRECT: Derive from base schema
export const createTaskInputSchema = taskSchema.pick({
  title: true,
  description: true,
});

// ❌ WRONG: Using `any` for untyped data
const data: any = await fetch("/api/tasks").then(r => r.json());

// ✅ CORRECT: Validate with Zod
const result = z.array(taskSchema).safeParse(await fetch("/api/tasks").then(r => r.json()));
if (result.success) {
  const data = result.data; // typed as Task[]
}

// ❌ WRONG: schema.parse() on untrusted external data without error handling
const task = taskSchema.parse(req.body); // throws if invalid — unhandled

// ✅ CORRECT: safeParse() with error handling
const result = taskSchema.safeParse(req.body);
if (!result.success) {
  return Response.json({ data: null, error: { message: "Validation failed", code: "VALIDATION_ERROR" } }, { status: 400 });
}
const task = result.data;

DO NOT

  • DO NOT hand-write TypeScript interface or type for data models — always use Zod schemas with z.infer.
  • DO NOT duplicate field definitions across schemas — use .pick(), .omit(), .extend(), .partial() to derive from the base.
  • DO NOT use any — validate untyped data with Zod schemas.
  • DO NOT use schema.parse() on untrusted data without error handling — use schema.safeParse() instead.
  • DO NOT put type files in schemas/ or schema files in types/ — keep them in their respective folders.
  • DO NOT create schemas without exporting them from the feature barrel index.ts.
  • DO NOT define enums as plain strings — use z.enum() for type safety and validation.
Skills Info
Original Name:zod-schema-patternsAuthor:madlado87