Zod API Validation Setup - Complete Guide
Overview
Your Constellation frontend now has comprehensive Zod-based validation for all FastAPI backend API calls. This ensures type safety and runtime validation across the entire application.
Files Created
1. Core Schemas
src/lib/schemas/api.ts- Main API schemas- All entity types (User, Constellation, Collection, etc.)
- Create/Update request schemas
- Array schemas for list endpoints
-
Auth and error schemas
-
src/lib/schemas/admin.ts- Admin-specific schemas - Dashboard stats, activity, alerts
- User management
- Permission management
- Analytics data
2. Utilities
src/lib/validation.ts- Validation helper functionsvalidateResponse()- Validate API responsesvalidateRequest()- Validate request bodiesvalidateAxiosResponse()- Combined validationtryValidateResponse()- Safe validation (returns null on failure)
3. Typed API Wrappers
src/lib/api.ts- Main API endpoint wrappers- Type-safe functions for all endpoints
- Automatic Zod validation on responses
-
Organized by resource (Constellations, Collections, Users, etc.)
-
src/lib/admin-api.ts- Admin-specific API wrappers - Dashboard endpoints
- User management
- Permission management
- Analytics endpoints
Usage Examples
Basic Example - Get Constellations
import { getConstellations } from "@/src/lib/api";
export async function MyComponent() {
try {
// Type-safe! Response is automatically validated
const constellations = await getConstellations();
// TypeScript knows the type:
constellations.forEach(c => {
console.log(c.id, c.name); // โ Valid properties
});
} catch (error) {
// Validation errors caught and reported
console.error("Failed to fetch:", error.message);
}
}
Create Example - With Validation
import { createConstellation } from "@/src/lib/api";
import { ConstellationCreate } from "@/src/lib/schemas/api";
export async function createNewConstellation(data: ConstellationCreate) {
try {
// Request is validated against schema
const created = await createConstellation(data);
console.log("Created:", created.id);
} catch (error) {
// Catches validation errors AND API errors
console.error(error.message);
}
}
Admin Dashboard Example
import { getDashboardStats, getDashboardUsers } from "@/src/lib/admin-api";
export async function AdminDashboard() {
try {
// Get stats
const stats = await getDashboardStats();
console.log(`Total users: ${stats.total_users}`);
// Get paginated users
const users = await getDashboardUsers({
page: 1,
limit: 50,
search: "john"
});
users.forEach(user => {
console.log(`${user.name}: ${user.email}`);
});
} catch (error) {
console.error("Admin error:", error.message);
}
}
Manual Validation Example
import { validateRequest } from "@/src/lib/validation";
import { ConstellationCreate } from "@/src/lib/schemas/api";
export async function validateInput(userInput: unknown) {
try {
// Manually validate request data
const validated = validateRequest(
userInput,
ConstellationCreate,
"User Input"
);
// Now it's safe to use
return validated;
} catch (error) {
console.error("Invalid input:", error.message);
throw error;
}
}
Safe Validation (No Throw)
import { tryValidateResponse } from "@/src/lib/validation";
import { UserPublic } from "@/src/lib/schemas/api";
export async function safeValidation(data: unknown) {
// Returns null if validation fails instead of throwing
const user = tryValidateResponse(data, UserPublic);
if (user) {
console.log("Valid user:", user.email);
} else {
console.log("Invalid user data");
}
}
API Endpoint Reference
Constellations
getConstellations() // Get all
getConstellation(id) // Get one
createConstellation(data) // Create
updateConstellation(id, data) // Update
deleteConstellation(id) // Delete
Collections
getCollections(constellationId) // Get all in constellation
getCollection(constellationId, id) // Get one
createCollection(constellationId, data) // Create
updateCollection(constellationId, id, data) // Update
deleteCollection(constellationId, id) // Delete
Users
getUsers() // Get all
getUser(id) // Get one
createUser(data) // Create
getCurrentUser() // Get current auth'd user
YDocs (Files/Documents)
getYDocs(collectionId) // Get all in collection
getYDoc(collectionId, ydocId) // Get one
createYDoc(collectionId, data) // Create
updateYDoc(collectionId, ydocId, data) // Update
deleteYDoc(collectionId, ydocId) // Delete
getYDocContent(collectionId, ydocId) // Get content
updateYDocContent(collectionId, ydocId, data) // Update content
Glossary
getGlossaryTerms(constellationId) // Get all terms
getGlossaryTerm(constellationId, termId) // Get one term
Admin Endpoints
getDashboardStats() // Stats overview
getDashboardActivity() // Recent activity
getDashboardAlerts() // System alerts
getDashboardUsers(query) // User management
getDashboardConstellations(query) // Constellation management
getDashboardPermissions(query) // Permission management
getAnalytics(query) // Analytics data
isUserAdmin(email) // Check admin status
toggleUserStatus(userId) // Toggle active/inactive
sendVerificationEmail(userId) // Send verification
Type Safety Features
1. Input Validation
All Create/Update operations validate request data:
// โ Valid
await createConstellation({
name: "My Project",
description: "A great project"
});
// โ Type error - missing required field
await createConstellation({ name: "My Project" });
2. Response Validation
All responses are validated against schemas:
// If API returns unexpected data structure,
// error is thrown immediately with details
const constellations = await getConstellations();
// Guaranteed to be ConstellationPublic[]
3. Inference from Schemas
Get types directly from Zod schemas:
import { ConstellationPublic, UserPublic } from "@/src/lib/schemas/api";
type MyUser = typeof UserPublic; // Type inferred from schema
type MyConstellation = typeof ConstellationPublic;
Error Handling
All validation errors include clear messages:
try {
await getConstellation("invalid-id");
} catch (error) {
// Error message: "Get Constellation validation failed:
// id: Invalid uuid, name: Required, ..."
console.error(error.message);
}
Migration Guide
Before (No Validation)
const response = await apiClient.get("/constellations");
// response.data could be anything
const constellations: any[] = response.data;
After (With Validation)
import { getConstellations } from "@/src/lib/api";
const constellations = await getConstellations();
// Type is guaranteed: ConstellationPublic[]
// Data is validated at runtime
Adding New Endpoints
When your backend adds new endpoints:
-
Add schema to
src/lib/schemas/api.ts:export const MyNewSchema = z.object({ id: z.string().uuid(), name: z.string(), }); export type MyNew = z.infer<typeof MyNewSchema>; -
Add wrapper function to
src/lib/api.ts:export async function getMyNewEndpoint(): Promise<MyNew> { const response = await apiClient.get("/my-new-endpoint"); return validateAxiosResponse(response, MyNewSchema, "Get My New Endpoint"); } -
Use it with full type safety:
const data = await getMyNewEndpoint(); // Fully typed and validated
Best Practices
-
Always use typed wrappers instead of raw
apiClient:// โ Good import { getUser } from "@/src/lib/api"; const user = await getUser(id); // โ Avoid const response = await apiClient.get(`/users/${id}`); const user = response.data; -
Let Zod catch errors early:
// โ Good - error caught at response level try { const user = await getUser(id); } catch (error) { console.error("Invalid response:", error.message); } -
Use type inference:
// โ Good - let TypeScript infer types const user = await getUser(id); // type is UserPublic // โ Avoid - redundant const user: UserPublic = await getUser(id); -
Pre-validate form inputs:
import { validateRequest } from "@/src/lib/validation"; import { ConstellationCreate } from "@/src/lib/schemas/api"; const formData = { name: "", description: "" }; try { const validated = validateRequest(formData, ConstellationCreate); await createConstellation(validated); } catch (error) { showFormError(error.message); }
Testing
All schemas are Zod schemas and can be tested:
import { ConstellationPublic } from "@/src/lib/schemas/api";
describe("ConstellationPublic schema", () => {
it("validates valid constellation", () => {
const valid = {
id: "123e4567-e89b-12d3-a456-426614174000",
name: "My Constellation",
description: "A great constellation"
};
expect(() => ConstellationPublic.parse(valid)).not.toThrow();
});
it("rejects invalid data", () => {
const invalid = { name: "My Constellation" };
expect(() => ConstellationPublic.parse(invalid)).toThrow();
});
});
Summary
You now have: โ Full type safety for all API calls โ Automatic runtime validation โ Clear error messages โ Centralized schema definitions matching FastAPI backend โ Easy to extend for new endpoints โ Zero-runtime-overhead type checking
Start using src/lib/api.ts and src/lib/admin-api.ts instead of raw apiClient calls!