Skip to content

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 functions
  • validateResponse() - Validate API responses
  • validateRequest() - Validate request bodies
  • validateAxiosResponse() - Combined validation
  • tryValidateResponse() - 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:

  1. 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>;
    

  2. 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");
    }
    

  3. Use it with full type safety:

    const data = await getMyNewEndpoint(); // Fully typed and validated
    

Best Practices

  1. 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;
    

  2. 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);
    }
    

  3. Use type inference:

    // โœ“ Good - let TypeScript infer types
    const user = await getUser(id); // type is UserPublic
    
    // โœ— Avoid - redundant
    const user: UserPublic = await getUser(id);
    

  4. 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!