Oliver White
2 May 2026 · 6 min read
At the API boundary — HTTP request bodies, query parameters, webhook payloads — TypeScript has no power. It enforces types within your codebase at compile time, but it cannot verify what arrives over the network at runtime. Zod fills that gap: it validates inputs at the edge of your system, before any Redis call or database query forms.
Consider this vote API handler. TypeScript is happy — it compiles cleanly. But what happens when an attacker sends a malformed request?
| Input | TypeScript (compile-time) | Zod (runtime) |
|---|---|---|
| "valid-uuid-here" | ✓ accepts string | ✓ accepts valid UUID |
| "not-a-uuid" | ✓ accepts string | ✗ rejects — not UUID format |
| undefined | ✗ type error (if typed) | ✗ rejects — required field |
| null | ✗ type error (if typed) | ✗ rejects — not a string |
| "; DROP TABLE votes;" | ✓ accepts string | ✗ rejects — not UUID format |
| 12345 (number) | ✗ type error | ✗ rejects — not a string |
UUIDs have a strict format: 8-4-4-4-12 hexadecimal characters. z.string().uuid() rejects anything that doesn't match — including SQL injection attempts, oversized strings, and type confusions. This eliminates an entire class of injection risk before the query even forms.
Every API route in AI Art Arena follows the same order: parse JSON, Zod validate, rate limit check, auth check, database call. This sequence is not arbitrary — it's designed to fail fast on cheap checks before doing expensive ones:
An attacker sending malformed payloads never touches Redis or the database. An anonymous user hitting the rate limit never triggers a database query. The order enforces a performance and security pyramid.
Built with this methodology
TypeScript catches type errors at compile time. At the API boundary — HTTP request bodies, query parameters, webhook payloads — TypeScript has no power. Zod validates at runtime, before anything touches Redis or the database.
From the build log