Runtime API Response Validation: Why TypeScript Isn't Enough
TypeScript only checks types at compile time. Data from APIs arrives at runtime with no guarantees it matches your interfaces. Without extra validation, mismatches show up as user-facing errors: undefined fields, wrong types, missing objects.
Problems Without Runtime Checks
TypeScript interfaces give a false sense of security. Here's typical code without validation:
interface User {
id: number;
name: string;
email: string;
}
const getUser = async (): Promise<User> => {
const res = await fetch('/api/user');
return res.json(); // No check on actual data
};
TypeScript trusts the type annotation and ignores the real server response. Key risks:
- Backend structure changes:
full_name→fullNameleads toundefined. - Type mismatches: string
"123"instead of number123causes concatenation instead of addition. - Conditional fields:
addressmissing in edge cases, crash onuser.address.city. - Third-party APIs with outdated docs.
Schema Validation Libraries
The fix? Runtime schema validation libraries. They parse real data and throw errors on mismatches. Top TypeScript options:
- Zod: Infers types from schemas, huge ecosystem.
- Yup: Works great with Formik.
- Valibot: Bundle-friendly, tree-shakable.
- Joi: Node.js roots, backend-focused.
- ArkType: Performance-first.
Implementing with Zod
Zod combines schema, types, and validation in one. User example:
import { z } from 'zod';
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
});
type User = z.infer<typeof UserSchema>;
const getUser = async (): Promise<User> => {
const res = await fetch('/api/user');
const data = await res.json();
return UserSchema.parse(data); // Errors on mismatch
};
parse() throws a ZodError with details on failure. Use safeParse() for an object with success and error.
Scaling in Real Projects
For large apps, centralize schemas for all endpoints. Wrap fetch like this:
const apiFetch = async <T>(url: string, schema: z.ZodSchema<T>): Promise<T> => {
const res = await fetch(url);
const data = await res.json();
return schema.parse(data);
};
const users = await apiFetch('/api/users', z.array(UserSchema));
Error handling: Log to Sentry, show user-friendly messages. For arrays, validate each item.
- Use
z.array(UserSchema)for lists. z.union()for polymorphic responses.z.lazy()for recursive structures.z.transform()to normalize data.
Key Takeaways
- TypeScript doesn't guarantee runtime types for external data: APIs, localStorage, URL params.
- Schema validation catches mismatches early, cutting debug time.
- Zod is the frontend standard: types from schemas, easy integration.
- In teams with separate backends, it prevents silent failures.
- Scale with fetch wrappers for consistency.
Runtime validation fills the gap between TypeScript's compile-time checks and execution. In production, it slashes bugs from out-of-sync API contracts.
— Editorial Team
No comments yet.