Back to list
petbrains

backend-zod

by petbrains

Document-Driven Development framework for Claude Code — structured specs, TDD cycles, feedback loops, and skills system

6🍴 1📅 Jan 24, 2026

SKILL.md


name: backend-zod description: TypeScript-first schema validation library. Use for ALL input validation in TypeScript projects — API inputs, form data, environment variables, config files. Essential companion for tRPC (required), React Hook Form, and any data boundary. Choose Zod when you need runtime validation with automatic TypeScript type inference. allowed-tools: Read, Edit, Write, Bash (*)

Zod (Schema Validation)

Overview

Zod is a TypeScript-first schema declaration and validation library. Define a schema once, get both runtime validation AND TypeScript types automatically via z.infer<>.

Version: Zod 4 (2025) / Zod 3.x widely used
Requirements: TypeScript ≥5.5, strict mode

Key Benefit: Single source of truth for validation and types — no drift between runtime checks and TypeScript.

When to Use This Skill

Use Zod when:

  • Validating API inputs (required for tRPC)
  • Building forms with react-hook-form
  • Parsing environment variables
  • Validating config files or JSON
  • Creating DTOs between layers
  • Any data crossing trust boundaries

Zod is NOT for:

  • Complex business rule validation (use domain logic)
  • Database schema definition (use Prisma schema)
  • Static type-only definitions (use interfaces)

Quick Start

Installation

npm install zod

Basic Usage

import { z } from 'zod';

// Define schema
const UserSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  age: z.number().int().positive().optional(),
});

// Infer TypeScript type
type User = z.infer<typeof UserSchema>;

// Validate
const result = UserSchema.safeParse(data);
if (result.success) {
  console.log(result.data); // typed as User
} else {
  console.log(result.error.issues);
}

Core Schema Patterns

Primitives with Validation

const EmailSchema = z.string().email('Invalid email format');
const PasswordSchema = z.string().min(8, 'Min 8 characters');
const AgeSchema = z.number().int().positive().max(150);
const UrlSchema = z.string().url();
const UuidSchema = z.string().uuid();

Object Schemas

const UserSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  name: z.string().min(2).max(100),
  role: z.enum(['user', 'admin', 'moderator']),
  createdAt: z.date(),
});

type User = z.infer<typeof UserSchema>;

Derive Variations

// For create (omit auto-generated fields)
const CreateUserSchema = UserSchema.omit({ id: true, createdAt: true });

// For update (all optional)
const UpdateUserSchema = CreateUserSchema.partial();

// For public response (only safe fields)
const PublicUserSchema = UserSchema.pick({ id: true, name: true });

Advanced Patterns

Discriminated Unions

Use for type-safe API responses:

const ApiResponse = z.discriminatedUnion('status', [
  z.object({ status: z.literal('success'), data: UserSchema }),
  z.object({ status: z.literal('error'), code: z.string(), message: z.string() }),
]);

type ApiResponse = z.infer<typeof ApiResponse>;
// TypeScript knows: if status === 'success', data exists

Custom Validation with Refine

const PasswordSchema = z.string()
  .min(8)
  .refine(val => /[A-Z]/.test(val), 'Must contain uppercase')
  .refine(val => /[0-9]/.test(val), 'Must contain number');

Cross-Field Validation with SuperRefine

const FormSchema = z.object({
  password: z.string(),
  confirmPassword: z.string(),
}).superRefine((data, ctx) => {
  if (data.password !== data.confirmPassword) {
    ctx.addIssue({
      code: 'custom',
      message: 'Passwords must match',
      path: ['confirmPassword'],
    });
  }
});

Transforms (Data Normalization)

// Normalize email
const NormalizedEmail = z.string()
  .email()
  .transform(s => s.toLowerCase().trim());

// Parse string to number
const StringToNumber = z.string()
  .transform(s => parseInt(s, 10))
  .pipe(z.number());

Coercion (API Query Parameters)

// GET requests receive strings — use coercion
const PaginationSchema = z.object({
  page: z.coerce.number().int().min(1).default(1),
  limit: z.coerce.number().int().min(1).max(100).default(20),
  active: z.coerce.boolean().optional(),
});

Common Schema Recipes

Pagination Input

export const PaginationSchema = z.object({
  limit: z.number().min(1).max(100).default(10),
  cursor: z.string().uuid().optional(),
});

Date Range Filter

export const DateRangeSchema = z.object({
  from: z.coerce.date(),
  to: z.coerce.date(),
}).refine(d => d.from <= d.to, 'From must be before To');

Environment Variables

const EnvSchema = z.object({
  NODE_ENV: z.enum(['development', 'production', 'test']),
  DATABASE_URL: z.string().url(),
  JWT_SECRET: z.string().min(32),
  PORT: z.coerce.number().default(3000),
});

export const env = EnvSchema.parse(process.env);

File Upload Metadata

const FileSchema = z.object({
  name: z.string(),
  size: z.number().max(10 * 1024 * 1024), // 10MB
  type: z.enum(['image/png', 'image/jpeg', 'application/pdf']),
});

Integration with tRPC

import { z } from 'zod';
import { publicProcedure } from '../trpc';

const CreateUserSchema = z.object({
  email: z.string().email(),
  name: z.string().min(2),
});

export const userRouter = router({
  create: publicProcedure
    .input(CreateUserSchema)  // ← Zod validates automatically
    .mutation(({ input }) => {
      // input is typed as { email: string; name: string }
    }),
});

Rules

Do ✅

  • Use z.infer<typeof Schema> to derive types
  • Use discriminated unions over regular unions for objects
  • Use .safeParse() when handling errors gracefully
  • Add descriptive error messages
  • Use .default() for optional fields with defaults
  • Keep schemas in dedicated files (/schemas/*.schema.ts)

Avoid ❌

  • Async transforms in tRPC input (not supported)
  • Using .catchall() with output schemas (inference issues)
  • Multiple Zod installations (causes type inference failures)
  • Overly complex nested refinements (hard to debug)

Parse Methods

MethodThrowsReturns
.parse(data)YesT
.safeParse(data)No{ success, data?, error? }
.parseAsync(data)YesPromise<T>
.safeParseAsync(data)NoPromise<{ success, data?, error? }>

Use .safeParse() in application code, .parse() in trusted contexts.


Troubleshooting

"Type inference not working":
  → Check single Zod installation: npm ls zod
  → Ensure TypeScript strict mode enabled
  → Restart TypeScript server

"Coercion not working":
  → Use z.coerce.number() not z.number() for query params
  → Check input is string before coercion

"Transform output type wrong":
  → Use .pipe() after transform for additional validation
  → Check transform return type

"Refinement errors unclear":
  → Add path parameter to ctx.addIssue()
  → Use descriptive error messages

File Structure

src/schemas/
├── user.schema.ts       # User-related schemas
├── post.schema.ts       # Post-related schemas
├── common.schema.ts     # Pagination, date ranges, etc.
└── env.schema.ts        # Environment validation

References

Score

Total Score

75/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

+10
説明文

100文字以上の説明がある

+10
人気

GitHub Stars 100以上

0/15
最近の活動

1ヶ月以内に更新

+10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

+5

Reviews

💬

Reviews coming soon