Validate at the boundary with Zod schemas and branded types. Business functions trust validated input.
Validates external input at boundaries using Zod schemas and branded types, so business functions can trust their arguments are already valid. Use this when handling HTTP requests, CLI arguments, or queue messages to reject malformed data before it reaches core logic.
/plugin marketplace add jagreehal/jagreehal-claude-skills/plugin install jagreehal-claude-skills@jagreehal-marketplaceThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Validation is a boundary concern. Check passports once at the border, not at every street corner.
External Input (HTTP, CLI, Queue) <- untrusted
|
v
Boundary Layer (validate with Zod) <- reject bad data here
|
v
Business Functions fn(args, deps) <- args ALREADY valid by contract
Validation checks data and returns true/false. Parsing transforms data into a new, richer type.
// Validation mindset: "Is this email valid?"
function isValidEmail(s: string): boolean { ... }
// Parsing mindset: "Give me an Email, or fail"
function parseEmail(s: string): Email { ... }
With parsing, you have an Email type that CANNOT be invalid by construction.
import { z } from 'zod';
const CreateUserSchema = z.object({
name: z.string().min(2).max(100),
email: z.string().email(),
});
type CreateUserInput = z.infer<typeof CreateUserSchema>;
const EmailSchema = z.string().email().brand<'Email'>();
const UserIdSchema = z.string().uuid().brand<'UserId'>();
type Email = z.infer<typeof EmailSchema>; // string & { __brand: 'Email' }
type UserId = z.infer<typeof UserIdSchema>; // string & { __brand: 'UserId' }
// Now TypeScript prevents accidental raw strings
function sendEmail(to: Email, subject: string) { ... }
sendEmail("alice@example.com", "Hello"); // ERROR: string not assignable to Email
sendEmail(EmailSchema.parse("alice@example.com"), "Hello"); // OK
| Use Branded Types | Use Plain Types |
|---|---|
IDs that look alike (userId, orderId) | Internal-only types |
| Security-sensitive values (tokens, keys) | Simple strings with no confusion risk |
| Values that MUST go through validation | Prototyping / early development |
| Cross-boundary data | Types only used in one function |
Rule of thumb: If mixing up two string parameters would cause a bug, brand them.
app.post('/users', async (req, res) => {
// 1. Validate at the boundary
const parsed = CreateUserSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json(formatZodError(parsed.error));
}
// 2. Call business function with valid, typed data
const user = await userService.createUser(parsed.data);
return res.status(201).json(user);
});
NO validation inside business functions. They trust args are already valid:
// CORRECT - No validation, trust the contract
async function createUser(
args: CreateUserInput, // Already validated!
deps: CreateUserDeps
): Promise<User> {
const user = { id: crypto.randomUUID(), ...args };
await deps.db.saveUser(user);
return user;
}
// WRONG - Validation mixed with business logic
async function createUser(args: { name: string; email: string }, deps) {
if (!args.name || args.name.length < 2) {
throw new Error('Name must be at least 2 characters'); // DON'T DO THIS
}
// ...
}
type ValidationErrorResponse = {
error: 'VALIDATION_FAILED';
message: string;
issues: Array<{
path: string;
message: string;
code: string;
}>;
};
function formatZodError(error: z.ZodError): ValidationErrorResponse {
return {
error: 'VALIDATION_FAILED',
message: 'Request validation failed',
issues: error.issues.map(issue => ({
path: issue.path.join('.'),
message: issue.message,
code: issue.code,
})),
};
}
| Type | Where | What | Tool |
|---|---|---|---|
| Schema Validation | Boundary | Shape, types, format, ranges | Zod |
| Domain Validation | Business function | Business rules (email exists, has permission) | Database lookups |
// Schema validation (boundary)
const TransferSchema = z.object({
fromAccount: z.string().uuid(),
toAccount: z.string().uuid(),
amount: z.number().positive(),
});
// Domain validation (business function)
async function validateTransfer(args: TransferInput, deps: TransferDeps) {
const account = await deps.db.getAccount(args.fromAccount);
if (account.balance < args.amount) {
return err('INSUFFICIENT_FUNDS'); // Business rule, not schema
}
// ...
}
const PaginationSchema = z.object({
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
});
// "?page=2&limit=50" -> { page: 2, limit: 50 }
const UpdateUserSchema = z.object({
name: z.string().min(2).optional(),
email: z.string().email().optional(),
});
const CreatePostSchema = z.object({
title: z.string().transform(s => s.trim()),
slug: z.string().transform(s => s.toLowerCase().replace(/\s+/g, '-')),
});
function validateBody<T>(schema: z.ZodSchema<T>) {
return (req: Request, res: Response, next: NextFunction) => {
const result = schema.safeParse(req.body);
if (!result.success) {
return res.status(400).json(formatZodError(result.error));
}
req.body = result.data;
next();
};
}
app.post('/users', validateBody(CreateUserSchema), async (req, res) => {
const user = await userService.createUser(req.body);
res.status(201).json(user);
});
| Question | Answer |
|---|---|
| Where validate shape/format? | Boundary (Zod schema) |
| Where validate business rules? | Business function |
| Should fn(args, deps) validate args? | NO. Trust the contract |
| Error for invalid input? | HTTP 400 (client error) |
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.