Skip to main content
Beta: Front-End Checklist is currently in beta. Some issues are still being fixed. Thanks for your patience.

Validate external data at runtime with a schema library

Use Zod or Valibot to validate data from API responses, form inputs, localStorage, and environment variables — TypeScript types are erased at runtime and cannot protect against unexpected shapes.

Utilities
Quick take
Typical fix time 30 min
  • TypeScript types are compile-time only — they are completely erased at runtime
  • API responses can differ from their declared types without causing a compile error
  • A Zod schema simultaneously validates data and infers the TypeScript type
  • Validate at trust boundaries only — not inside every internal function call
Why it matters: TypeScript gives you confidence at compile time, but data from the network, user input, and storage arrives at runtime as raw, untyped values. A backend schema change, a misconfigured API, or malicious input can produce data that does not match your TypeScript types — and the compiler will never warn you. Runtime validation with a schema library catches these mismatches at the boundary, surfaces clear error messages, and prevents type-unsafe data from propagating through your application.

Rule Details

TypeScript types are a compile-time tool. When your code runs in the browser or on a server, every type annotation is gone, which is why libraries like Zod (opens in new tab) and Valibot (opens in new tab) exist to validate runtime data before it spreads through the rest of the app.

Code Example

// The TypeScript type — looks safe
interface Product {
  id: string
  name: string
  price: number
  inStock: boolean
}
 
// ❌ Unsafe: we cast the response to our type without checking
async function fetchProduct(id: string): Promise<Product> {
  const res = await fetch(`/api/products/${id}`)
  const data = await res.json()
  return data as Product // the API might return { id: number, price: "12.99" }
}
 
// TypeScript is happy — but this crashes at runtime:
const product = await fetchProduct('abc')
const total = product.price * 1.2 // NaN if price is actually the string "12.99"

Why It Matters

TypeScript gives you confidence at compile time, but data from the network, user input, and storage arrives at runtime as raw, untyped values. A backend schema change, a misconfigured API, or malicious input can produce data that does not match your TypeScript types — and the compiler will never warn you. Runtime validation at the same boundary where you would otherwise call JSON.parse() (opens in new tab) catches these mismatches early, surfaces clear error messages, and prevents type-unsafe data from propagating through your application.

API Response Validation with Zod

import { z } from 'zod'
 
// 1. Define the schema — single source of truth
const ProductSchema = z.object({
  id: z.string(),
  name: z.string().min(1),
  price: z.number().positive(),
  inStock: z.boolean(),
  // Coerce types that the API might send differently
  createdAt: z.coerce.date(),
})
 
// 2. Infer the TypeScript type from the schema — no duplication
type Product = z.infer<typeof ProductSchema>
 
// 3. Validate at the boundary
async function fetchProduct(id: string): Promise<Product> {
  const res = await fetch(`/api/products/${id}`)
  if (!res.ok) {
    throw new Error(`Failed to fetch product: ${res.status}`)
  }
  const json: unknown = await res.json()
  return ProductSchema.parse(json) // throws ZodError with clear message if invalid
}

Safe Parsing for Graceful Error Handling

Use .safeParse() when you want to handle the failure without throwing:

async function fetchProducts(): Promise<Product[] | null> {
  const res = await fetch('/api/products')
  const json: unknown = await res.json()
 
  const result = z.array(ProductSchema).safeParse(json)
 
  if (!result.success) {
    // result.error is a ZodError with field-level detail
    console.error('API response shape mismatch:', result.error.flatten())
    return null
  }
 
  return result.data // fully typed: Product[]
}

Form Validation

import { z } from 'zod'
 
const ContactFormSchema = z.object({
  name: z.string().min(2, 'Name must be at least 2 characters'),
  email: z.string().email('Enter a valid email address'),
  message: z.string().min(10).max(1000),
  // Enum validation
  subject: z.enum(['support', 'billing', 'feedback']),
  // Optional with default
  newsletter: z.boolean().default(false),
})
 
type ContactForm = z.infer<typeof ContactFormSchema>
 
function handleSubmit(formData: FormData): void {
  const raw = Object.fromEntries(formData.entries())
 
  const result = ContactFormSchema.safeParse(raw)
  if (!result.success) {
    // Field-level errors for the UI
    const errors = result.error.flatten().fieldErrors
    displayErrors(errors)
    return
  }
 
  submitContact(result.data) // result.data is typed as ContactForm
}

Environment Variable Validation

Validate environment variables at startup so misconfiguration is caught immediately rather than at the point of use hours later.

import { z } from 'zod'
 
const EnvSchema = z.object({
  NODE_ENV: z.enum(['development', 'test', 'production']),
  DATABASE_URL: z.string().url(),
  API_SECRET: z.string().min(32, 'API_SECRET must be at least 32 characters'),
  PORT: z.coerce.number().int().positive().default(3000),
  FEATURE_NEW_DASHBOARD: z.enum(['true', 'false']).transform(v => v === 'true').optional(),
})
 
// Call once at application startup — throws if any required variable is missing
export const env = EnvSchema.parse(process.env)
 
// Usage — fully typed, no string indexing into process.env
console.log(`Listening on port ${env.PORT}`)
Validate once, at the boundary

Runtime validation is meant for trust boundaries — where data enters your application from the outside world. Do not add Zod schemas to every internal function call between modules you control; that adds noise without improving safety. The pattern is: validate once at the edge (API layer, form handler, startup), then pass typed values through your application.

localStorage Validation

const ThemePreferenceSchema = z.object({
  mode: z.enum(['light', 'dark', 'system']),
  fontSize: z.number().min(12).max(24).default(16),
})
 
type ThemePreference = z.infer<typeof ThemePreferenceSchema>
 
function loadThemePreference(): ThemePreference {
  try {
    const stored = localStorage.getItem('theme')
    if (!stored) return ThemePreferenceSchema.parse({}) // uses defaults
    return ThemePreferenceSchema.parse(JSON.parse(stored))
  } catch {
    // Stored value was invalid JSON or wrong shape — use defaults
    localStorage.removeItem('theme')
    return ThemePreferenceSchema.parse({})
  }
}

Valibot — a Smaller Alternative

Valibot offers the same validation patterns with a tree-shakeable, modular API that produces smaller bundle sizes:

import { object, string, number, boolean, parse, email, minLength } from 'valibot'
 
const UserSchema = object({
  id: string(),
  email: string([email()]),
  name: string([minLength(1)]),
  age: number(),
  active: boolean(),
})
 
type User = typeof UserSchema._types.output
 
const user = parse(UserSchema, apiResponse)

Exceptions

  • A framework default or browser behavior is not an exception by itself; only documented constraints with compensating controls should suppress the finding.
  • When a JavaScript pattern looks unsafe but the data is fully constrained, validated, and never attacker-controlled, document that boundary explicitly instead of treating it as implicit.
  • If a rule overlaps with a stronger exploit path or runtime failure, fix the issue that most directly enables compromise or user-visible breakage first.

Verification

  1. Search for every fetch() call, JSON.parse(), and localStorage.getItem() in the codebase and confirm each one passes the result through a Zod .parse() or .safeParse() call before the data is used.
  2. Verify that TypeScript types for external data are derived with z.infer<typeof Schema> or the equivalent Valibot (opens in new tab) output types rather than written separately — duplicate type definitions drift apart over time.
  3. Confirm environment variable access goes through a validated env object rather than direct process.env string indexing.
  4. Run the application with an intentionally malformed API response (e.g., using a mock server) and confirm the schema error is caught and handled gracefully rather than propagating as a runtime crash.

Use with AI

Copy these prompts to use with your AI assistant, or install the MCP server to use directly from Claude, Cursor, or Windsurf.

Check

Verify implementation

Identify all places in this code where external data enters the application (fetch calls, localStorage reads, env variable access, form submissions) and report which ones lack runtime schema validation.

Fix

Auto-fix issues

Add Zod schemas to validate the external data entry points in this code. Show the schema definition, the validated type inference, and where to call .parse() or .safeParse().

Explain

Learn more

Explain why TypeScript types do not protect against runtime data mismatches, how Zod bridges compile-time and runtime safety, and when to use .parse() versus .safeParse().

Review

Code review

Review all external data entry points in this file: API calls, storage reads, environment variable access, and form handling. Flag any location where data is cast to a TypeScript type without a preceding runtime validation step.

Sources

References used to support the guidance in this rule.

Rules that often go hand-in-hand with this one.

Avoid the any type — use unknown, generics, or type guards instead

Replace TypeScript's any type with unknown, proper generics, or narrowed type assertions to preserve type safety without sacrificing expressiveness.

JavaScript
Enable TypeScript strict mode in tsconfig.json

Enable "strict": true in tsconfig.json to activate the full suite of TypeScript type-checking flags and catch the most common runtime bugs at compile time.

JavaScript
Implement proper error handling

Use try-catch blocks and error boundaries to gracefully handle errors in async operations and UI components.

JavaScript
Parse JSON safely with error handling

Always wrap JSON.parse() in try/catch and validate the parsed structure before use, as invalid JSON or unexpected data shapes cause runtime errors.

JavaScript

Was this rule helpful?

Your feedback helps improve rule quality. This stays internal for now.

Loading feedback...
0 / 385