AdvancedTypeScript · Lesson 3 of 10

Runtime Validation with Zod

Types disappear at runtime — validate external data and infer types from schemas.

TypeScript types are erased when code runs. Anything coming from outside your program — HTTP bodies, environment variables, files, third-party APIs — can be any shape, regardless of what your types say.

A schema library such as Zod validates data at runtime and gives you the TypeScript type for free with z.infer, so the schema is the single source of truth.

Validate at the boundaries (where data enters), then pass fully typed, trusted values to the rest of your code. Use safeParse to get a result object instead of an exception, and return the issues to the client as a 400 response.

TerminalShell
npm install zod
validation.tsTypeScript
import { z } from "zod";

const RegisterSchema = z.object({
  name: z.string().trim().min(2, "Name is too short"),
  email: z.string().email(),
  phone: z.string().regex(/^0[67]\d{8}$/, "Use a Tanzanian mobile number like 0712345678"),
  form: z.coerce.number().int().min(1).max(6),
  subjects: z.array(z.string()).min(1).max(10),
});

type RegisterInput = z.infer<typeof RegisterSchema>;

function register(input: RegisterInput): string {
  return input.name + " registered for Form " + input.form;
}

const body: unknown = {
  name: "  Amina ",
  email: "amina@example.com",
  phone: "0712345678",
  form: "4",
  subjects: ["Maths", "Biology"],
};

const parsed = RegisterSchema.safeParse(body);
if (parsed.success) {
  console.log(register(parsed.data)); // name trimmed, form coerced to number
} else {
  console.log(parsed.error.flatten().fieldErrors);
}

const EnvSchema = z.object({
  PORT: z.coerce.number().default(3000),
  NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
});
export const env = EnvSchema.parse(process.env);

Key points

  • Types are compile-time only; validate every external input at runtime.
  • z.infer derives the TypeScript type from the schema — no duplication.
  • Validate environment variables at startup so misconfiguration fails fast.

Exercise

Add Zod validation to the HTTP server from the Intermediate track: a CreateTaskSchema (title 1–120 chars, optional due date as ISO string, optional priority enum). Return 400 with field errors when validation fails.

Show solution

Try the exercise yourself first — then compare your approach with this one.

Define the schema once and derive the type from it with z.infer. In the route, safeParse the body: on failure return 400 with flatten().fieldErrors, so the client sees exactly which field is wrong.

create-task.tsTypeScript
import { z } from "zod";

const CreateTaskSchema = z.object({
  title: z.string().trim().min(1, "Title is required").max(120),
  dueDate: z.string().datetime().optional(),
  priority: z.enum(["low", "normal", "high"]).default("normal"),
});

type CreateTaskInput = z.infer<typeof CreateTaskSchema>;

function handleCreate(body: unknown): { status: number; body: unknown } {
  const parsed = CreateTaskSchema.safeParse(body);
  if (!parsed.success) {
    return { status: 400, body: { errors: parsed.error.flatten().fieldErrors } };
  }
  const task: CreateTaskInput & { id: number } = { id: 1, ...parsed.data };
  return { status: 201, body: task };
}

console.log(JSON.stringify(handleCreate({ title: "  Revise vectors ", dueDate: "2026-11-01T08:00:00Z" })));
// {"status":201,"body":{"id":1,"title":"Revise vectors","dueDate":"2026-11-01T08:00:00Z","priority":"normal"}}
console.log(JSON.stringify(handleCreate({ title: "", priority: "urgent" })));
// {"status":400,"body":{"errors":{"title":["Title is required"],"priority":["Invalid enum value. ..."]}}}

Check your understanding

  1. Why aren't TypeScript types enough to validate an HTTP request body?

  2. What does z.infer<typeof Schema> give you?

  3. What is the difference between parse and safeParse?

  4. Why validate environment variables at startup?

Ask AI