Claude Code Plugins

Community-maintained marketplace

Feedback

Use this skill when backend API contracts change and frontend types need synchronization. Triggers on: Pydantic model changes, REST endpoint updates, WebSocket message formats, or GraphQL schema modifications. Dynamically detects contract type from context. NOT for unrelated type definitions or internal backend-only changes.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md


name: api-contract-sync description: Use this skill when backend API contracts change and frontend types need synchronization. Triggers on: Pydantic model changes, REST endpoint updates, WebSocket message formats, or GraphQL schema modifications. Dynamically detects contract type from context. NOT for unrelated type definitions or internal backend-only changes.

API Contract Sync

Overview

Automatically detect and synchronize API contracts between backend and frontend when data structures, endpoints, or message formats change. Ensures type safety and prevents runtime errors from contract mismatches.

Core Principle: Context-Driven Detection

This skill does not assume Pydantic ↔ TypeScript. It:

  1. Detects contract type from project context (REST, WebSocket, GraphQL, gRPC)
  2. Identifies source of truth (backend models, OpenAPI spec, GraphQL schema)
  3. Determines target (TypeScript types, Zod schemas, client SDKs)
  4. Syncs appropriately based on detected patterns

When to Use

Invoke this skill when:

  • Backend model changes: Pydantic models, SQLAlchemy models, dataclasses modified
  • Endpoint modifications: New fields, renamed properties, changed response structure
  • Message format updates: WebSocket events, SSE payloads, message queue schemas
  • API versioning: New API version with breaking changes
  • Type drift detected: Frontend using outdated or incorrect types

DO NOT use when:

  • Internal backend types (not exposed via API)
  • Frontend-only type definitions (UI state, component props)
  • No communication between backend and frontend
  • Types already synchronized manually

Contract Detection Workflow

Step 1: Identify Contract Type

Analyze project structure to determine contract mechanism:

Detection heuristics:

# Check project files and structure
if exists("backend/app/models/*.py") and uses("pydantic"):
    contract_type = "Pydantic → TypeScript"
    source_files = glob("backend/app/models/*.py")
    target_files = glob("frontend/types/*.ts") or "frontend/src/types/"

elif exists("backend/schema.graphql"):
    contract_type = "GraphQL Schema → TypeScript"
    source_files = ["backend/schema.graphql"]
    target_files = ["frontend/generated/graphql.ts"]

elif exists("backend/openapi.json") or exists("backend/openapi.yaml"):
    contract_type = "OpenAPI → TypeScript"
    source_files = ["backend/openapi.json"]
    target_files = ["frontend/api/client.ts"]

elif exists("backend/app/websocket.py"):
    contract_type = "WebSocket Messages → TypeScript"
    source_files = ["backend/app/websocket.py"]  # Message definitions
    target_files = ["frontend/types/websocket.ts"]

else:
    contract_type = "Unknown - manual analysis required"

Output contract type to user for confirmation before proceeding.

Step 2: Extract Backend Contract

Based on detected type, extract API contract:

Type A: Pydantic → TypeScript

Source: Pydantic models used in API responses

# Example: backend/app/models/task.py
from pydantic import BaseModel

class Task(BaseModel):
    id: int
    title: str
    completed: bool
    created_at: datetime

Extraction: Parse Pydantic model fields and types

Mapping:

  • intnumber
  • strstring
  • boolboolean
  • datetimestring (ISO8601)
  • Optional[T]T | null
  • list[T]T[]

Type B: GraphQL Schema → TypeScript

Source: GraphQL schema file

type Task {
  id: ID!
  title: String!
  completed: Boolean!
  createdAt: DateTime!
}

Extraction: Use GraphQL codegen or parse schema directly

Tool: @graphql-codegen/cli (if configured)

Type C: WebSocket Messages → TypeScript

Source: WebSocket message classes/types

# backend/app/websocket.py
class NotificationEvent(BaseModel):
    type: Literal["notification"]
    message: str
    timestamp: datetime
    user_id: int

Extraction: Parse message definitions, create discriminated unions

Type D: OpenAPI → TypeScript

Source: OpenAPI spec (usually auto-generated from FastAPI)

components:
  schemas:
    Task:
      type: object
      properties:
        id: { type: integer }
        title: { type: string }
        completed: { type: boolean }

Extraction: Use OpenAPI TypeScript generators

Tool: openapi-typescript or swagger-typescript-api

Step 3: Generate Frontend Types

Based on contract type, generate appropriate frontend types:

For Pydantic → TypeScript (Manual)

// frontend/types/task.ts
export interface Task {
  id: number;
  title: string;
  completed: boolean;
  created_at: string;  // ISO8601 datetime
}

export interface TaskCreate {
  title: string;
  completed?: boolean;
}

export interface TaskUpdate {
  title?: string;
  completed?: boolean;
}

For GraphQL (Codegen)

# Use GraphQL Code Generator
npx graphql-codegen --config codegen.yml

Generates frontend/generated/graphql.ts automatically

For WebSocket Messages

// frontend/types/websocket.ts
export type WebSocketMessage =
  | { type: "notification"; message: string; timestamp: string; user_id: number }
  | { type: "status_update"; status: string }
  | { type: "ping" };

export interface NotificationEvent {
  type: "notification";
  message: string;
  timestamp: string;
  user_id: number;
}

For OpenAPI (Codegen)

# Use OpenAPI TypeScript generator
npx openapi-typescript http://localhost:8000/openapi.json -o frontend/types/api.ts

Step 4: Validate Synchronization

After sync, verify:

Validation checklist:

  • All backend models used in API have corresponding frontend types
  • Field names match exactly (or mapping documented)
  • Data types are compatible (no stringnumber mismatches)
  • Optional/required fields match
  • Nested types resolved correctly
  • No circular dependencies in type definitions

Validation methods:

  1. Type-check frontend: Run tsc --noEmit or npm run typecheck
  2. Runtime validation: Use Zod or similar to validate API responses
  3. Integration tests: Test actual API calls with typed responses

Step 5: Handle Mismatches

When backend and frontend types diverge:

Mismatch scenarios:

  1. Breaking change: Backend removed field frontend uses

    Solution: Version API (/v1 vs /v2) or add deprecated field temporarily
    
  2. Field renamed: Backend created_atcreatedAt

    Solution: Add alias in Pydantic model or transform in API layer
    
  3. Type changed: intstr (e.g., ID changed to UUID)

    Solution: Breaking change - version API, update all consumers
    
  4. New required field: Backend added required field

    Solution: Make optional temporarily, or provide default value
    

Resolution workflow:

Detect mismatch
↓
Assess impact (breaking vs non-breaking)
├─ Non-breaking (new optional field) → Add to frontend types
├─ Breaking (removed/changed field) → Version API or add migration path
└─ Critical (security/data integrity) → Block until resolved

Contract Sync Patterns

Pattern 1: Pydantic Models → TypeScript (Direct)

When: Simple FastAPI project, manual type management

Workflow:

  1. Read Pydantic models in backend/app/models/
  2. Generate equivalent TypeScript interfaces
  3. Save to frontend/types/
  4. Import in frontend code

Example:

# backend/app/models/user.py
class User(BaseModel):
    id: int
    email: str
    role: Literal["admin", "user"]

// frontend/types/user.ts
export interface User {
  id: number;
  email: string;
  role: "admin" | "user";
}

Pattern 2: OpenAPI → TypeScript (Automated)

When: FastAPI auto-generates OpenAPI, want full automation

Workflow:

  1. Start backend server (generates OpenAPI at /openapi.json)
  2. Run openapi-typescript to generate types
  3. Commit generated types to version control
  4. CI/CD verifies no manual edits to generated files

Setup:

// package.json
{
  "scripts": {
    "generate:api": "openapi-typescript http://localhost:8000/openapi.json -o src/types/api.ts"
  }
}

Pattern 3: GraphQL Schema → TypeScript (Codegen)

When: Using GraphQL, schema-first approach

Workflow:

  1. Define GraphQL schema in backend/schema.graphql
  2. Run GraphQL Code Generator
  3. Use generated types + hooks in React components

Config:

# codegen.yml
schema: http://localhost:8000/graphql
generates:
  frontend/generated/graphql.ts:
    plugins:
      - typescript
      - typescript-operations
      - typescript-react-apollo

Pattern 4: WebSocket Messages → Discriminated Unions

When: Real-time features with multiple message types

Workflow:

  1. Define message types in backend (Pydantic models)
  2. Create TypeScript discriminated union
  3. Use type guards for runtime type narrowing

Backend:

class PingMessage(BaseModel):
    type: Literal["ping"] = "ping"

class NotificationMessage(BaseModel):
    type: Literal["notification"] = "notification"
    message: str
    user_id: int

WebSocketMessage = PingMessage | NotificationMessage

Frontend:

export type WebSocketMessage =
  | { type: "ping" }
  | { type: "notification"; message: string; user_id: number };

function handleMessage(msg: WebSocketMessage) {
  if (msg.type === "notification") {
    // TypeScript knows msg has message and user_id here
    console.log(msg.message);
  }
}

Automation Strategies

Strategy 1: Pre-commit Hook

Generate types before every commit:

# .git/hooks/pre-commit
#!/bin/bash
npm run generate:api
git add frontend/types/

Strategy 2: CI/CD Validation

Verify types are synchronized:

# .github/workflows/type-check.yml
- name: Generate API types
  run: npm run generate:api

- name: Check for drift
  run: |
    git diff --exit-code frontend/types/
    # Fails if generated types differ from committed

Strategy 3: Development Watch Mode

Auto-regenerate on backend changes:

// package.json
{
  "scripts": {
    "dev": "concurrently \"backend\" \"npm run watch:types\"",
    "watch:types": "nodemon --watch backend/app/models --exec 'npm run generate:api'"
  }
}

Advanced Topics

Handling Nested Types

Backend:

class Address(BaseModel):
    street: str
    city: str

class User(BaseModel):
    id: int
    address: Address

Frontend:

export interface Address {
  street: string;
  city: string;
}

export interface User {
  id: number;
  address: Address;  // Nested type
}

Handling Generics

Backend:

from typing import Generic, TypeVar

T = TypeVar("T")

class PaginatedResponse(BaseModel, Generic[T]):
    items: list[T]
    total: int
    page: int

Frontend:

export interface PaginatedResponse<T> {
  items: T[];
  total: number;
  page: number;
}

// Usage
type TasksPage = PaginatedResponse<Task>;

Handling Enums

Backend:

from enum import Enum

class TaskStatus(str, Enum):
    TODO = "todo"
    IN_PROGRESS = "in_progress"
    DONE = "done"

Frontend:

export enum TaskStatus {
  TODO = "todo",
  IN_PROGRESS = "in_progress",
  DONE = "done",
}

// Or as union type
export type TaskStatus = "todo" | "in_progress" | "done";

Anti-Patterns

  • Manual duplication: Retyping backend models manually (error-prone)
  • Ignoring mismatches: Frontend types drift from backend reality
  • Over-engineering: Creating complex mapping layers for simple projects
  • No validation: Trusting types without runtime checks
  • Hardcoded types: Not using contract as single source of truth

Examples

Example 1: Pydantic Model Change

Scenario: Added priority field to Task model

Backend change:

class Task(BaseModel):
    id: int
    title: str
    completed: bool
    priority: Literal["low", "medium", "high"] = "medium"  # NEW

Sync action:

// frontend/types/task.ts
export interface Task {
  id: number;
  title: string;
  completed: boolean;
  priority: "low" | "medium" | "high";  // ADDED
}

Verification: tsc --noEmit passes ✓

Example 2: WebSocket Message Addition

Scenario: Added new task_updated event

Backend:

class TaskUpdatedEvent(BaseModel):
    type: Literal["task_updated"] = "task_updated"
    task_id: int
    updated_fields: dict[str, Any]

Frontend:

export type WebSocketMessage =
  | { type: "ping" }
  | { type: "notification"; message: string }
  | { type: "task_updated"; task_id: number; updated_fields: Record<string, any> }; // NEW

Example 3: Breaking Change (Field Removed)

Scenario: Removed completed field, replaced with status

Backend:

class Task(BaseModel):
    id: int
    title: str
    status: Literal["todo", "in_progress", "done"]  # Replaces 'completed'

Sync strategy:

  1. Immediate: Add migration - keep completed as computed property

    @property
    def completed(self) -> bool:
        return self.status == "done"
    
  2. Gradual: Version API (/v1 has completed, /v2 has status)

  3. Breaking: Update all frontend consumers, remove old field

Integration with Other Skills

  • parallel-coordinator: Ensures backend and frontend agents sync types
  • fastapi-backend-expert: Generates Pydantic models this skill syncs
  • React Frontend Expert (F1): Consumes synced types in components
  • artifact-aggregator: Documents contract changes in reports

Notes

  • Contract type detection is context-driven, not hardcoded
  • Prefer automation (OpenAPI codegen) over manual sync for large projects
  • Always validate sync with type checker + runtime validation (Zod)
  • Breaking changes require API versioning or careful migration
  • Keep generated types in version control for audit trail