fern client

why

a hand-written or wrapped client drifts from the real route graph the moment the backend renames an operation or changes a response shape; the drift surfaces only at runtime in a consumer. generating the typed client from the live OpenAPI document makes the compiler the verifier of the transport contract: breaking changes fail at build time, not production.

rule

  • Fern is the only authored-to-consumer transport bridge: live route graph exports OpenAPI -> Fern generates the typed client -> consumers call the generated operation. one vocabulary for operation names, request shapes, response schemas, diagnostics.
  • an authored wrapper is allowed only for a real boundary concern: server credential ownership, retry policy, response validation, or a consumer-specific projection. a wrapper that merely renames or forwards a generated method is an alias and must not exist.
  • hand-written clients that escape the generation boundary diverge silently: the backend renames an operation, adds a required parameter, or changes a response field, and the consumer discovers it only at runtime. instead, regenerate from the live OpenAPI and update every caller — the compiler catches the mismatch before deployment.

change flow

interface outcome
-> named Zod/OpenAPI route contract
-> generated OpenAPI document
-> Fern client and types
-> query hook or server composition
-> consuming interface
  • the operation name is a navigation landmark: consumer-specific, generated spelling through every layer.
  • do not shorten it at the frontend or map it into a second local DTO.

case study: one operation through a process panel

a named active-status operation on the live route graph reaches the generated client used by one query hook. the hook validates the shared response schema and uses its base-package query key. the interface knows one typed operation and one displayable projection — not the route string, transport envelope layout, or persistence composition.

verification sequence

  1. change the named route schema and live route graph.
  2. generate the OpenAPI document from that graph.
  3. run Fern generation; never edit its output.
  4. update every caller, query hook, response validator, and cache key using the generated symbols.
  5. typecheck the SDK and consumer, then exercise the consuming interface or focused route smoke path.

do not

  • hand-edit generated/, dist/, exported OpenAPI, or generated route types.
  • write a second raw HTTP client for a contract Fern already generates.
  • copy generated request/response types into local aliases.
  • let a frontend route string or response envelope drift from the live route.
  • preserve a renamed operation as a client-side compatibility wrapper.

good and bad

bad

// old wrapper from before Fern was adopted
export async function getWorkflowStatus(workflowId: string) {
return fetch(`/api/workflows/${workflowId}/status`).then((response) => response.json());
}
// consumer relies on the wrapper's implicit contract
const status = await getWorkflowStatus(workflowId);
console.log(status.progress); // consumer assumes 'progress' field

backend refactors the route to require an API key parameter and renames progress to percentComplete. the wrapper sees no changes; the fetch call silently omits the key and the consumer reads an undefined field.

good

// regenerated from OpenAPI after backend changes
const result = await apiClient.workflow.readActiveWorkflowStatus({
workflowId,
apiKey: config.key, // compiler requires new parameter
});
// TypeScript error: 'progress' does not exist, use 'percentComplete' instead
console.log(result.percentComplete);

the generated operation carries the one authored contract name and response shape into the consuming boundary: a backend contract change forces recompilation and every consumer updates together.

search pages

go to any page