The problem with untyped data
Your API has a trust problem
Picture this. You build an API endpoint that accepts a contact form submission. The endpoint expects a name and an email. The frontend sends { "name": "Alice", "email": "[email protected]" }, and everything works beautifully. Ship it.
Then someone sends this:
// What you expect:
{ "name": "Alice", "email": "[email protected]" }
// What you might actually get:
{ "name": 42, "email": "" }
{ "nme": "Alice" } // typo in the field name
"not even an object"
null Your API has no idea what just arrived. The request body is raw bytes from the internet. After you call JSON.parse, it becomes a JavaScript value, but TypeScript has zero clue what shape that value is. JSON.parse returns any.
And any is where things start to break.
Why any is dangerous
const body = JSON.parse(rawBody);
// body is `any` — TypeScript provides zero type safety
console.log(body.name.toUpperCase());
// If body.name is 42, this throws: 42.toUpperCase is not a function
// If body.name is undefined, this throws: Cannot read properties of undefined Here is the thing about any: TypeScript completely gives up on type checking. If you access .name on an any value, TypeScript does not warn you. It trusts you. But the data came from the internet, and you should not trust it.
This is the core problem. TypeScript protects you at compile time, but any creates a blind spot. Your code compiles fine, then crashes at runtime because someone sent a number where you expected a string.
It is not just request bodies
You might think this is only about POST requests, but the problem shows up everywhere:
Request bodies are JSON sent by clients. Could be literally anything.
Query parameters are always strings. ?page=2 gives you the string "2", not the number 2. Try doing math with that.
Path parameters work the same way. /books/book-1 extracts "book-1" as a string. Is that a valid ID? You have no way of knowing.
External API responses are JSON from a third-party service. Did the shape change since you last checked? Maybe. APIs evolve, documentation gets stale, and schemas drift without warning.
Environment variables like process.env.PORT are string | undefined. Is it a valid number? Is it even defined?
Config files are JSON or YAML read from disk. Are all the required fields there?
Every single source of external data has this same problem: TypeScript cannot know the shape, and the data might be wrong.
You could validate by hand
One approach is to write manual validation:
function validateContact(data: unknown): { name: string; email: string } {
if (typeof data !== "object" || data === null) {
throw new Error("Expected an object");
}
const obj = data as Record<string, unknown>;
if (typeof obj.name !== "string") {
throw new Error("name must be a string");
}
if (obj.name.length === 0) {
throw new Error("name cannot be empty");
}
if (typeof obj.email !== "string") {
throw new Error("email must be a string");
}
if (!obj.email.includes("@")) {
throw new Error("email must be valid");
}
return { name: obj.name, email: obj.email };
} This works. For two fields. Now imagine doing this for 10 fields, nested objects, arrays of objects, optional fields, and default values, across multiple endpoints. The manual validation code grows faster than the actual business logic. It is tedious, fragile, and the kind of code nobody wants to maintain.
There is a better way
This is where Zod comes in. Zod is a TypeScript-first schema validation library. You describe the expected shape of your data (that is called a schema), and Zod validates incoming data against it:
import { z } from "zod/v4";
const ContactSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
});
const result = ContactSchema.safeParse(body);
if (!result.success) {
console.log(result.error); // Detailed error: which field, what went wrong
} else {
console.log(result.data); // { name: "Alice", email: "[email protected]" } — typed!
} Three lines replace that entire manual validation function. And the result is fully typed. TypeScript knows that result.data.name is a string and result.data.email is a string. No any, no guessing, no runtime surprises.
In the next lesson, we will break down exactly how schemas work, what parse and safeParse do, and how to start validating your first pieces of data.
Exercises
Exercise 1: Write a manual validation function for { title: string, rating: number }. Count the lines of code. Now think about adding optional fields, nested objects, and arrays.
Exercise 2: Call JSON.parse('{"name": 42}'). Try to call .toUpperCase() on the name field. Observe the runtime error that TypeScript did not catch.
Exercise 3: List five sources of untrusted data in a typical web application.
Why does JSON.parse return 'any' in TypeScript?