{"slug": "cursor-driven-development-in-fastapi-using-ai-to-generate-type-safe-api-schemas", "title": "Cursor-Driven Development in FastAPI: Using AI to Generate Type-Safe API Schemas and Catch Contract Breaks Before Deployment", "summary": "A developer at CitizenApp built a system using Claude AI to generate both FastAPI backend schemas and TypeScript frontend types from natural language specifications, preventing API contract mismatches that previously caused production crashes and lost customer trust. The approach treats a single markdown specification file as the source of truth, with AI generating Pydantic models and TypeScript types that are validated against each other in CI before deployment. This method caught a type mismatch between a React 19 frontend expecting `{ id: string; email: string }` and a FastAPI backend returning `{ userId: number; userEmail: string }` before it reached production.", "body_md": "API contracts break silently. Your React 19 frontend calls `POST /users`\n\nexpecting `{ id: string; email: string }`\n\n, but your FastAPI backend shipped `{ userId: number; userEmail: string }`\n\n. The request succeeds. The response doesn't match. Your frontend crashes in production, and you spend three hours in an incident call figuring out why a perfectly typed TypeScript component is receiving undefined properties.\n\nI've lived this exact nightmare twice on CitizenApp—once before we shipped, once after. The second time cost us a customer's trust and three hours of developer velocity.\n\nThe fix isn't better testing or more code review. It's treating your API contract as the single source of truth and using Claude to generate *both* sides of it from natural language specifications, then validating that they match in CI before deployment. This is cursor-driven development done right: not mindless boilerplate generation, but intelligent contract enforcement that catches type mismatches across language boundaries.\n\nMost full-stack developers reach for AI to generate TypeScript components or FastAPI route handlers. That's fine. But the real power is forcing your frontend and backend to negotiate a contract before either side exists. When you do this correctly:\n\nI prefer this approach because it forces discipline. Too many teams use AI as a shortcut to avoid thinking about their API design. This pattern does the opposite: it requires you to think first, validate second.\n\nHere's how it works on CitizenApp:\n\nLet's build this end-to-end.\n\n```\nFeature: User Profile Update\nDescription: Allow authenticated users to update their profile metadata.\nFields: firstName (string, required), lastName (string, required), bio (string, optional, max 500 chars)\nValidation: firstName and lastName must be at least 2 characters\nResponse: Returns updated user object with id, email, firstName, lastName, bio, updatedAt\nAuth: Requires valid JWT in Authorization header\n```\n\nThis goes in a file: `specs/user-profile-update.md`\n\nPrompt your Cursor AI agent (or use Anthropic's API directly):\n\n```\nGiven this spec, generate a FastAPI route with Pydantic schema. \nUse SQLAlchemy ORM patterns. \nInclude validation decorators.\nOutput ONLY Python code, no explanation.\n\n[paste spec here]\n```\n\nClaude outputs something like:\n\n``` python\n# schemas.py\nfrom pydantic import BaseModel, Field, field_validator\n\nclass UserProfileUpdateRequest(BaseModel):\n    firstName: str = Field(..., min_length=2, max_length=100)\n    lastName: str = Field(..., min_length=2, max_length=100)\n    bio: str | None = Field(None, max_length=500)\n\n    @field_validator(\"firstName\", \"lastName\")\n    @classmethod\n    def validate_names(cls, v):\n        return v.strip()\n\nclass UserProfileResponse(BaseModel):\n    id: str\n    email: str\n    firstName: str\n    lastName: str\n    bio: str | None\n    updatedAt: str  # ISO 8601\n\n    class Config:\n        from_attributes = True\npython\n# routes.py\nfrom fastapi import APIRouter, Depends, HTTPException\nfrom sqlalchemy.orm import Session\nfrom .schemas import UserProfileUpdateRequest, UserProfileResponse\nfrom .db import get_db\nfrom .auth import get_current_user\n\nrouter = APIRouter()\n\n@router.put(\"/users/profile\", response_model=UserProfileResponse)\nasync def update_user_profile(\n    request: UserProfileUpdateRequest,\n    current_user = Depends(get_current_user),\n    db: Session = Depends(get_db),\n):\n    user = db.query(User).filter(User.id == current_user.id).first()\n    if not user:\n        raise HTTPException(status_code=404, detail=\"User not found\")\n\n    user.firstName = request.firstName\n    user.lastName = request.lastName\n    if request.bio is not None:\n        user.bio = request.bio\n\n    db.commit()\n    db.refresh(user)\n    return user\n```\n\nYou review this, tweak it if needed, and commit it. This is now your source of truth.\n\nDifferent prompt:\n\n```\nGiven this API spec and the Pydantic schemas below, generate:\n1. TypeScript types matching the schemas (use zod or ts-pattern if validation needed)\n2. A React 19 hook using useAsync or similar pattern\n\nUse TanStack Query for mutations if appropriate.\nAssume JWT is in Authorization header (handled by interceptor).\n\nPydantic schema:\n[paste schema here]\n\nAPI spec:\n[paste spec here]\n```\n\nClaude outputs:\n\n```\n// types/user.ts\nexport interface UserProfileUpdateRequest {\n  firstName: string;\n  lastName: string;\n  bio?: string;\n}\n\nexport interface UserProfileResponse {\n  id: string;\n  email: string;\n  firstName: string;\n  lastName: string;\n  bio?: string;\n  updatedAt: string;\n}\njs\n// hooks/useUpdateUserProfile.ts\nimport { useMutation, useQueryClient } from \"@tanstack/react-query\";\n\nexport function useUpdateUserProfile() {\n  const queryClient = useQueryClient();\n\n  return useMutation({\n    mutationFn: async (\n      data: UserProfileUpdateRequest\n    ): Promise<UserProfileResponse> => {\n      const response = await fetch(\"/api/users/profile\", {\n        method: \"PUT\",\n        headers: {\n          \"Content-Type\": \"application/json\",\n          Authorization: `Bearer ${localStorage.getItem(\"token\")}`,\n        },\n        body: JSON.stringify(data),\n      });\n\n      if (!response.ok) {\n        throw new Error(`Failed to update profile: ${response.statusText}`);\n      }\n\n      return response.json();\n    },\n    onSuccess: (data) => {\n      queryClient.setQueryData([\"user\"], data);\n    },\n  });\n}\n```\n\nThis is where the magic happens. Create a Python script that introspects both:\n\n``` python\n# scripts/validate_contracts.py\nimport json\nimport subprocess\nimport sys\nfrom typing import Any\n\ndef get_pydantic_schema(module_path: str, class_name: str) -> dict[str, Any]:\n    \"\"\"Extract JSON schema from Pydantic model\"\"\"\n    import importlib.util\n    spec = importlib.util.spec_from_file_location(\"module\", module_path)\n    module = importlib.util.module_from_spec(spec)\n    spec.loader.exec_module(module)\n    model_class = getattr(module, class_name)\n    return model_class.model_json_schema()\n\ndef get_typescript_types(file_path: str) -> dict[str, Any]:\n    \"\"\"Parse TypeScript types using TypeScript compiler API\"\"\"\n    result = subprocess.run(\n        [\"npx\", \"ts-json-schema-generator\", \"--path\", file_path, \"--type\", \"*\"],\n        capture_output=True,\n        text=True,\n    )\n    return json.loads(result.stdout)\n\ndef validate_contract(python_schema: dict, ts_schema: dict) -> bool:\n    \"\"\"Ensure Python and TypeScript schemas match\"\"\"\n    py_props = python_schema.get(\"properties\", {})\n    ts_props = ts_schema.get(\"properties\", {})\n\n    # Check all Python props exist in TypeScript\n    for key, py_prop in py_props.items():\n        if key not in ts_props:\n            print(f\"❌ Missing in TypeScript: {key}\")\n            return False\n\n        # Map Python types to TypeScript\n        py_type = py_prop.get(\"type\")\n        ts_type = ts_props[key].get(\"type\")\n        if py_type != ts_type:\n            print(f\"❌ Type mismatch for {key}: Python={py_type}, TS={ts_type}\")\n            return False\n\n    return True\n\nif __name__ == \"__main__\":\n    py_schema = get_pydantic_schema(\"app/schemas.py\", \"UserProfileUpdateRequest\")\n    ts_schema = get_typescript_types(\"src/types/user.ts\")\n\n    if not validate_contract(py_schema, ts_schema):\n        sys.exit(1)\n\n    print(\"✅ Contract validated\")\n```\n\nAdd this to your GitHub Actions workflow:\n\n```\n- name: Validate API contracts\n  run: python scripts/validate_contracts.py\n\n- name: Run tests\n  run: pytest app/ && npm test\n```\n\nNow, if someone changes a Pydantic field from `str`\n\nto `int`\n\nwithout updating the TypeScript type, **the build fails before merging**. This has saved us countless production incidents.\n\nThis burned me hard: I made a field optional in Pydantic (`bio: str | None`\n\n) but forgot to mark it optional in TypeScript (`bio: string`\n\ninstead of `bio?: string`\n\n). The validation passed because my naive script only checked *required* fields.\n\nThe fix: validate both presence and optionality:\n\n``` php\npython\ndef validate_contract(python_schema: dict, ts_schema: dict) -> bool:\n    py_required = set(python_schema.get\n```\n\n", "url": "https://wpnews.pro/news/cursor-driven-development-in-fastapi-using-ai-to-generate-type-safe-api-schemas", "canonical_source": "https://dev.to/uaslimcreate/cursor-driven-development-in-fastapi-using-ai-to-generate-type-safe-api-schemas-and-catch-contract-55m7", "published_at": "2026-05-31 07:47:28+00:00", "updated_at": "2026-05-31 08:11:42.726347+00:00", "lang": "en", "topics": ["ai-tools", "ai-products", "artificial-intelligence", "large-language-models", "generative-ai"], "entities": ["FastAPI", "React", "TypeScript", "Claude", "CitizenApp"], "alternates": {"html": "https://wpnews.pro/news/cursor-driven-development-in-fastapi-using-ai-to-generate-type-safe-api-schemas", "markdown": "https://wpnews.pro/news/cursor-driven-development-in-fastapi-using-ai-to-generate-type-safe-api-schemas.md", "text": "https://wpnews.pro/news/cursor-driven-development-in-fastapi-using-ai-to-generate-type-safe-api-schemas.txt", "jsonld": "https://wpnews.pro/news/cursor-driven-development-in-fastapi-using-ai-to-generate-type-safe-api-schemas.jsonld"}}