DEV Community

myougaTheAxo
myougaTheAxo

Posted on

Input Validation with Claude Code: Zod Schemas for Every API Endpoint

The Problem: Claude Code Without Validation Rules

Without constraints, Claude Code generates code with common validation mistakes:

  • Passing req.body fields directly to a database without type checks
  • Using scattered manual checks like typeof req.body.name === 'string'
  • No validation at all on query parameters or nested objects

Specifying Zod as the standard in CLAUDE.md fixes this at the source.


Define Validation Rules in CLAUDE.md

## Validation Policy

- Use Zod for all input validation (no manual if-checks like typeof/instanceof)
- Place Zod schemas in `src/schemas/` — keep them separate from route handlers
- Use `z.safeParse()` instead of `z.parse()` to handle errors gracefully
- Use `.strict()` to reject unknown fields
- Return HTTP 422 Unprocessable Entity for validation errors
- Error body format: `{ errors: [{ field: string, message: string }] }`
Enter fullscreen mode Exit fullscreen mode

Once this is in CLAUDE.md, Claude Code will generate Zod schemas automatically every time it creates a new API endpoint.


Generating Zod Schemas with Claude Code

Prompt

Add a POST /api/products endpoint.
Create a Zod schema in src/schemas/product.schema.ts and apply it
through a validation middleware in the route handler.
Enter fullscreen mode Exit fullscreen mode

Generated Code

src/schemas/product.schema.ts

import { z } from "zod";

export const CreateProductSchema = z
  .object({
    name: z.string().trim().min(1, "Name is required").max(100),
    price: z.number().int().positive("Price must be a positive integer"),
    category: z.enum(["electronics", "clothing", "food"], {
      errorMap: () => ({ message: "Invalid category" }),
    }),
    description: z.string().trim().max(500).optional(),
    tags: z.array(z.string().trim().min(1)).max(10).default([]),
  })
  .strict(); // reject unknown fields

export type CreateProductInput = z.infer<typeof CreateProductSchema>;
Enter fullscreen mode Exit fullscreen mode

src/middleware/validate.ts

import { Request, Response, NextFunction } from "express";
import { ZodSchema } from "zod";

export function validate(schema: ZodSchema) {
  return (req: Request, res: Response, next: NextFunction) => {
    const result = schema.safeParse(req.body);
    if (!result.success) {
      return res.status(422).json({
        errors: result.error.issues.map((issue) => ({
          field: issue.path.join("."),
          message: issue.message,
        })),
      });
    }
    req.body = result.data; // replace with parsed, type-safe data
    next();
  };
}
Enter fullscreen mode Exit fullscreen mode

src/routes/products.ts

import { Router } from "express";
import { validate } from "../middleware/validate";
import { CreateProductSchema } from "../schemas/product.schema";

const router = Router();

router.post("/", validate(CreateProductSchema), async (req, res) => {
  // req.body is fully typed as CreateProductInput here
  const { name, price, category } = req.body;
  res.status(201).json({ message: "Created" });
});
Enter fullscreen mode Exit fullscreen mode

Unified Error Response Format

Prompt to enforce consistent errors

Return all validation errors as HTTP 422.
Format: { errors: [{ field: string, message: string }] }
Never expose raw Zod Issue objects in the response.
Enter fullscreen mode Exit fullscreen mode

Example Response

HTTP/1.1 422 Unprocessable Entity

{
  "errors": [
    { "field": "name", "message": "Name is required" },
    { "field": "price", "message": "Price must be a positive integer" }
  ]
}
Enter fullscreen mode Exit fullscreen mode

The client can map errors by field name and display them next to the corresponding form input.


Validating Query Parameters

Query parameters arrive as strings, so use z.coerce to convert them safely:

export const ListQuerySchema = z.object({
  page: z.coerce.number().int().min(1).default(1),
  limit: z.coerce.number().int().min(1).max(100).default(20),
  search: z.string().trim().optional(),
});

router.get("/", (req, res) => {
  const result = ListQuerySchema.safeParse(req.query);
  if (!result.success) {
    return res.status(422).json({ errors: result.error.issues });
  }
  const { page, limit, search } = result.data;
  // ...
});
Enter fullscreen mode Exit fullscreen mode

Nested Object Validation

export const CreateOrderSchema = z.object({
  items: z
    .array(
      z.object({
        productId: z.string().uuid(),
        quantity: z.number().int().min(1).max(99),
      })
    )
    .min(1, "At least one item is required"),
  shippingAddress: z.object({
    postalCode: z.string().regex(/^\d{5}(-\d{4})?$/, "Invalid postal code format"),
    city: z.string().min(1),
    street: z.string().min(1),
  }),
});
Enter fullscreen mode Exit fullscreen mode

Catch Validation Gaps with Hooks

Use a Claude Code hook to warn when a route handler is written without a validation middleware call:

.claude/hooks/pre-tool-call.sh

#!/bin/bash
TOOL_NAME="$1"
FILE_PATH="$2"

if [[ "$TOOL_NAME" == "write_file" && "$FILE_PATH" == *"routes/"* ]]; then
  CONTENT=$(cat)
  if echo "$CONTENT" | grep -qE "router\.(post|put|patch)" && \
     ! echo "$CONTENT" | grep -q "validate("; then
    echo "WARNING: Route is missing a validation middleware call." >&2
    echo "Apply validate() with a Zod schema before proceeding." >&2
  fi
fi
Enter fullscreen mode Exit fullscreen mode

This runs before Claude Code writes the file, giving you a chance to catch missing validation before it lands in the codebase.


Summary

Practice Benefit
Zod standard in CLAUDE.md Schemas generated automatically
safeParse + 422 response Consistent, safe error handling
src/schemas/ directory Reusable, centralized schemas
Hook for validation gaps Catches missing validation early

Validation is unglamorous but it is where most API security issues begin. Telling Claude Code the rules upfront means you stop reviewing for it manually.


Code Review Pack (¥980) includes /code-review for automated validation gap detection. 👉 https://prompt-works.jp

Top comments (0)