# Validation Boundary

> Validates untrusted input once at the system boundary with Zod schemas and branded types, so business functions trust their args by contract. Use when handling HTTP request bodies, query params, CLI args, queue messages, env vars, or third-party API responses; when defining Zod schemas or branded types; or when deciding where validation belongs in a TypeScript app.

- **Type:** Skill
- **Install:** `agentstack add skill-jagreehal-jagreehal-claude-skills-validation-boundary`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [jagreehal](https://agentstack.voostack.com/s/jagreehal)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [jagreehal](https://github.com/jagreehal)
- **Source:** https://github.com/jagreehal/jagreehal-claude-skills/tree/main/skills/validation-boundary

## Install

```sh
agentstack add skill-jagreehal-jagreehal-claude-skills-validation-boundary
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Validation at the Boundary

## Overview

Validation is a **boundary concern**. You check passports once at the border, not at every street corner. Untrusted input (HTTP bodies, query params, CLI args, queue messages, env vars, third-party responses) is parsed and rejected at the edge of the system. Everything inside the boundary trusts its types by contract.

```
External Input (HTTP, CLI, Queue, 3rd-party)  ;
```

### 2. Use Branded Types for Stronger Guarantees

```typescript
const EmailSchema = z.string().email().brand();
const UserIdSchema = z.string().uuid().brand();

type Email = z.infer;   // string & { __brand: 'Email' }
type UserId = z.infer; // 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
```

#### When to Use Branded Types vs Plain Types

| 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.

### 3. Validate at HTTP/Queue/CLI Boundaries

```typescript
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);
});
```

### 4. Business Functions Trust the Contract

NO validation inside business functions. They trust args are already valid:

```typescript
// CORRECT - No validation, trust the contract
async function createUser(
  args: CreateUserInput,  // Already validated!
  deps: CreateUserDeps
): Promise {
  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 ;
};

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,
    })),
  };
}
```

## Two Layers of Validation

| Type | Where | What | Tool |
|------|-------|------|------|
| **Schema Validation** | Boundary | Shape, types, format, ranges | Zod |
| **Domain Validation** | Business function | Business rules (email exists, has permission) | Database lookups |

```typescript
// 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  { page: 2, limit: 50 }
```

### Partial Updates (PATCH)

```typescript
const UpdateUserSchema = z.object({
  name: z.string().min(2).optional(),
  email: z.string().email().optional(),
});
```

### Transforms

```typescript
const CreatePostSchema = z.object({
  title: z.string().transform(s => s.trim()),
  slug: z.string().transform(s => s.toLowerCase().replace(/\s+/g, '-')),
});
```

### Express Middleware

```typescript
function validateBody(schema: z.ZodSchema) {
  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);
});
```

## Quick Reference

| 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) |

## Common Rationalizations

| Rationalization | Reality |
|---|---|
| "I'll just check the input inside the function too, to be safe" | Double validation means neither layer is authoritative and the function now has two jobs. Parse once at the boundary; trust the type after. |
| "It came from our own database, but I'll validate it anyway" | Data that already crossed a boundary (or originated internally) is trusted. Re-validating it is noise that hides where the real boundary is. |
| "The third-party API always returns the right shape" | External services are untrusted. They change, fail, and can return malicious or instruction-like content. Parse their responses like any other boundary input. |
| "A plain string is fine for the user ID" | If two same-typed values can be swapped by mistake (userId vs orderId), brand them so the compiler catches the mix-up. |
| "Throwing inside the business function is simpler than returning a Result for the bad-balance case" | Schema validation belongs at the boundary; *business-rule* failures (insufficient funds, not found) are expected outcomes: return them as `result-types`, don't throw. |

## Red Flags

- `if (!args.email)` or `.length  r.json())` result used without parsing its shape
- Raw `string` parameters for IDs, tokens, or other easily-confused values
- A Zod schema defined but only used for types, never `.parse()`d at the edge
- Validation logic duplicated across multiple handlers instead of shared middleware/schema

## Verification

After wiring up input handling:

- [ ] Every external input is parsed with a Zod schema at the boundary
- [ ] Business functions accept already-validated types and contain no shape checks
- [ ] IDs/tokens that could be confused use branded types
- [ ] Third-party responses are parsed before use
- [ ] Invalid input returns a consistent error (HTTP 400 / `VALIDATION_FAILED`)
- [ ] Business-rule failures are returned as Results, not thrown (see [`result-types`](../result-types/SKILL.md))
- [ ] No re-validation of internal or database-sourced data

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [jagreehal](https://github.com/jagreehal)
- **Source:** [jagreehal/jagreehal-claude-skills](https://github.com/jagreehal/jagreehal-claude-skills)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-jagreehal-jagreehal-claude-skills-validation-boundary
- Seller: https://agentstack.voostack.com/s/jagreehal
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
