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
- change the named route schema and live route graph.
- generate the OpenAPI document from that graph.
- run Fern generation; never edit its output.
- update every caller, query hook, response validator, and cache key using the generated symbols.
- 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 adoptedexport async function getWorkflowStatus(workflowId: string) {return fetch(`/api/workflows/${workflowId}/status`).then((response) => response.json());}// consumer relies on the wrapper's implicit contractconst 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 changesconst result = await apiClient.workflow.readActiveWorkflowStatus({workflowId,apiKey: config.key, // compiler requires new parameter});// TypeScript error: 'progress' does not exist, use 'percentComplete' insteadconsole.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.