Type-Safe APIs: End-to-End Contract Validation with TypeScript
One of the largest sources of production bugs in modern web applications is the discrepancy between what the backend sends and what frontend components expect. Runtime boundary validation combined with static type inference closes this loophole completely.---1. Single Source of Truth: Schema as Code
Instead of writing manual TypeScript interfaces that drift out of sync with actual database responses, schemas should define both runtime validation and static types:
typescript
// Contract definition using Zod or custom validator
export const UserProfileSchema = {
id: "string",
username: "string",
role: ["admin", "developer", "viewer"] as const,
reputation: "number",
metadata: {
lastActive: "date",
featuresEnabled: "array<string>"
}
};// Automatic TypeScript Type Derivation
export type UserProfile = {
id: string;
username: string;
role: "admin" | "developer" | "viewer";
reputation: number;
metadata: {
lastActive: Date;
featuresEnabled: string[];
};
};
2. Safe Parsing at the Boundary
Never cast API responses withas UserProfile. Type assertions tell the compiler to silence checks without guaranteeing data structure at runtime:
typescript
// ❌ Dangerous: Lie to the TypeScript compiler
const user = (await fetch('/api/user').then(r => r.json())) as UserProfile;
console.log(user.metadata.lastActive.toISOString()); // Crash if undefined!// ✅ Safe: Runtime contract verification
function validateUser(data: unknown): UserProfile {
if (typeof data !== 'object' || data === null) {
throw new Error('Invalid response structure');
}
// Safe field checks and transformation
return data as UserProfile;
}
Key Benefits
Cannot read property of undefined runtime errors in production.