Skip to main content

API Integration

Maybern uses Orval to generate type-safe API clients from the backend OpenAPI specification.

Generated Clients

API clients are generated in src/gen/api/:
gen/api/
├── models/           # TypeScript types for all models
├── endpoints/        # API functions (queries, mutations)
└── schemas/          # Zod schemas for validation

Using Queries

import {
  useFundFamilyList,
  useFundFamilyRetrieve,
} from "@/gen/api";

// List all fund families
function FundFamilyList() {
  const { data, isLoading, error } = useFundFamilyList();
  
  if (isLoading) return <Spinner />;
  if (error) return <ErrorAlert error={error} />;
  
  return (
    <List>
      {data?.results.map((ff) => (
        <ListItem key={ff.id}>{ff.name}</ListItem>
      ))}
    </List>
  );
}

// Get single fund family
function FundFamilyDetail({ id }: { id: string }) {
  const { data: fundFamily } = useFundFamilyRetrieve(id);
  return <h1>{fundFamily?.name}</h1>;
}

Using Mutations

import { useFundFamilyCreate, useFundFamilyUpdate } from "@/gen/api";

function CreateFundFamily() {
  const createMutation = useFundFamilyCreate();
  
  const handleCreate = async (formData: FundFamilyCreateData) => {
    try {
      const result = await createMutation.mutateAsync({
        data: formData,
      });
      toast.success(`Created ${result.name}`);
      navigate(routes.fundFamily.detail({ id: result.id }));
    } catch (error) {
      toast.error("Failed to create fund family");
    }
  };
  
  return (
    <FundFamilyForm
      onSubmit={handleCreate}
      isLoading={createMutation.isPending}
    />
  );
}

Query Options

// With query options
const { data } = useFundFamilyList({
  query: {
    enabled: !!customerId,
    refetchInterval: 30000,
    staleTime: 5000,
  },
});

// With parameters
const { data } = useTransactionList({
  status: "completed",
  page: 1,
  pageSize: 25,
});

Error Handling

function DataComponent() {
  const { data, error, isError } = useFundFamilyList();
  
  if (isError) {
    // Error is typed
    if (error.response?.status === 404) {
      return <NotFound />;
    }
    if (error.response?.status === 403) {
      return <Forbidden />;
    }
    return <GenericError error={error} />;
  }
  
  return <Data data={data} />;
}

Cache Invalidation

import { useQueryClient } from "@tanstack/react-query";

function UpdateButton({ id }: { id: string }) {
  const queryClient = useQueryClient();
  const updateMutation = useFundFamilyUpdate();
  
  const handleUpdate = async (data: UpdateData) => {
    await updateMutation.mutateAsync({ id, data });
    
    // Invalidate related queries
    queryClient.invalidateQueries({ queryKey: ["fundFamily"] });
  };
}

Regenerating Clients

After backend API changes:
# Regenerate from OpenAPI spec
pnpm gen:api

# Or regenerate everything
pnpm gen
Never edit files in src/gen/. They will be overwritten on regeneration.

Custom Hooks

Wrap generated hooks for reusable patterns:
// hooks/useFundFamilyWithDefaults.ts
export function useFundFamilyWithDefaults(id: string) {
  const { data, ...rest } = useFundFamilyRetrieve(id);
  
  return {
    fundFamily: data,
    displayName: data?.name ?? "Loading...",
    isActive: data?.status === "active",
    ...rest,
  };
}

Orval Configuration

Configuration in orval.config.ts:
export default {
  maybern: {
    input: "../backend/frontend-maybern.yml",
    output: {
      mode: "tags-split",
      target: "./src/gen/api",
      schemas: "./src/gen/api/models",
      client: "react-query",
      override: {
        mutator: {
          path: "./src/shared/api/axios.ts",
          name: "customInstance",
        },
      },
    },
  },
};