Skip to main content
ship before you scale

Prompt Engineering for Frontend and the Codegen Pipeline

5 min read Chapter 9 of 42

Prompt Engineering for Frontend and the Codegen Pipeline

The Feature

The frontend uses a typed API client generated from the FastAPI OpenAPI schema. Developers never write fetch calls by hand. Every API call is type-safe, and changes to the backend schema produce TypeScript compilation errors in the frontend immediately.

The Decision

Orval for API client generation. Orval reads the FastAPI OpenAPI schema (auto-generated at /openapi.json) and produces TypeScript types and TanStack Query hooks. The alternative, writing fetch calls by hand and maintaining TypeScript interfaces manually, creates a synchronization problem between backend and frontend types. When the backend changes a field name, the hand-written frontend types do not break at compile time. They break at runtime, in production, when a customer reports that the page is blank.

The Implementation

Orval Configuration

// frontend/orval.config.ts
import { defineConfig } from "orval";

export default defineConfig({
  marketflow: {
    input: {
      target: "http://localhost:8000/openapi.json",
    },
    output: {
      target: "./src/api/generated.ts",
      client: "react-query",
      mode: "single",
      override: {
        mutator: {
          path: "./src/api/client.ts",
          name: "customInstance",
        },
        query: {
          useQuery: true,
          useMutation: true,
        },
      },
    },
  },
});
// frontend/src/api/client.ts
import Axios, { AxiosRequestConfig } from "axios";

const AXIOS_INSTANCE = Axios.create({
  baseURL: import.meta.env.VITE_API_URL,
});

// Add Supabase auth token to every request
AXIOS_INSTANCE.interceptors.request.use(async (config) => {
  const {
    data: { session },
  } = await supabase.auth.getSession();
  if (session?.access_token) {
    config.headers.Authorization = `Bearer ${session.access_token}`;
  }
  return config;
});

export const customInstance = <T>(config: AxiosRequestConfig): Promise<T> => {
  const promise = AXIOS_INSTANCE(config).then(({ data }) => data);
  return promise;
};
// Add to frontend/package.json scripts
{
  "scripts": {
    "generate-api": "orval",
    "dev": "vite",
    "build": "orval && tsc && vite build"
  }
}

Running npm run generate-api produces a file with typed hooks:

// frontend/src/api/generated.ts (auto-generated, do not edit)

export interface VendorApplicationRead {
  id: string;
  vendor_business_name: string;
  vendor_email: string;
  market_id: string;
  status: "pending" | "accepted" | "rejected" | "waitlisted";
  message: string;
  created_at: string;
}

export const useGetMarketApplications = (
  marketId: string,
  options?: UseQueryOptions<VendorApplicationRead[]>,
) => {
  return useQuery({
    queryKey: ["markets", marketId, "applications"],
    queryFn: () =>
      customInstance<VendorApplicationRead[]>({
        url: `/api/markets/${marketId}/applications`,
        method: "GET",
      }),
    ...options,
  });
};

export const useUpdateApplicationStatus = () => {
  return useMutation({
    mutationFn: (params: {
      marketId: string;
      applicationId: string;
      data: { status: "accepted" | "rejected" | "waitlisted" };
    }) =>
      customInstance<VendorApplicationRead>({
        url: `/api/markets/${params.marketId}/applications/${params.applicationId}`,
        method: "PATCH",
        data: params.data,
      }),
  });
};

React Component Prompt Template

Generate a React component for [feature]:

COMPONENT: [name]
PURPOSE: [what it displays or does]
DATA: [which API hooks it uses from generated.ts]
INTERACTIONS: [user actions and their effects]

RULES:
- TypeScript, functional component, no class components
- Use TanStack Query hooks from generated.ts for data fetching
- Use React Hook Form for any forms
- Use shadcn/ui components (Button, Card, Table, Dialog, Input, etc.)
- Handle loading state with a skeleton, not a spinner
- Handle error state with an error message, not a blank screen
- Handle empty state with a helpful message
- No 'any' type. No type assertions without a comment.
- Destructure props in the function signature
- Use useCallback for event handlers passed to child components

Frontend Review Checklist Additions

The backend review checklist from Chapter 3 applies to all generated code. These items are specific to frontend code:

1. Loading and Error States

  • Does the component handle the loading state from TanStack Query?
  • Does it handle the error state?
  • Does it handle the empty state (data loaded, but array is empty)?

2. Authentication

  • Does the component check that the user is authenticated before rendering?
  • Does it redirect to login if the session is expired?

3. Re-render Safety

  • Does the component use useCallback for handlers passed as props?
  • Does it avoid creating new object references in render (causes child re-renders)?
  • Does it work correctly in React Strict Mode (effects run twice)?

4. Type Safety

  • Are all props typed with an interface?
  • Are API response types from the generated client, not hand-written?
  • Is there any as type assertion that could mask a runtime error?

The Trap

// TRAP: Hand-written types that drift from the backend
interface Application {
  id: string;
  vendorName: string; // Backend changed this to vendor_business_name
  status: string; // Backend uses a union type, not string
  marketId: number; // Backend uses UUID (string), not number
}

// This compiles fine. It breaks at runtime.
// SAFE: Types generated from the OpenAPI schema
// VendorApplicationRead is auto-generated by orval
import {
  VendorApplicationRead,
  useGetMarketApplications,
} from "@/api/generated";

// If the backend changes a field name, this file regenerates
// and TypeScript shows compilation errors everywhere the old name is used.

The codegen pipeline is the single most impactful decision for frontend development speed. It eliminates an entire category of bugs (type mismatches between frontend and backend) and makes backend API changes visible as compilation errors, not runtime errors.

The Cost

ToolCost
Orval$0 (open source)
Axios$0 (open source)
API codegen time~5 seconds per run

The codegen adds 5 seconds to the build process and saves hours of debugging type mismatches across the frontend/backend boundary.