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.
npm install zodimport { 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.inferderives 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.
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. ..."]}}}