TypeScript caught the bug at compile time. Great. But what happens when the API returns something unexpected at runtime? Production bug.
The Problem
We had this TypeScript code:
interface User {
id: number;
name: string;
email: string;
}
function processUser(user: User) {
// TypeScript guarantees user exists and has these fields
sendEmail(user.email, 'Welcome!');
}
Production returned: { id: null, name: undefined }
TypeScript didn’t save us. The API changed, but our code broke.
Solution: Runtime Validation
I use Zod for runtime validation:
import { z } from 'zod';
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
});
// Parse at API boundary
const user = UserSchema.parse(apiResponse); // throws if invalid
Integration with Fastify/Express
app.get('/user/:id', async (req, res) => {
const user = await fetchUser(req.params.id);
// Validate before processing
const validated = UserSchema.safeParse(user);
if (!validated.success) {
return res.status(500).json({ error: 'Invalid data from upstream' });
}
// Now safe to use
processUser(validated.data);
});
The Pattern
My new approach:
- Define schemas with Zod (single source of truth)
- Generate TypeScript types from schemas
- Validate at every API boundary
- Trust but verify
Is It Worth It?
Extra code? Yes. Sleep better at night? Also yes. One time, a payment service returned { amount: "NaN" }. Zod caught it. TypeScript didn’t.