pyreon

Lathe covers what @hey-api/openapi-ts and its plugins generate — types, an SDK, TanStack Query helpers, zod schemas — as one configuration with one set of names. lathe init reads openapi-ts.config.ts; this page covers the code that calls the client.

The examples use the Bookshelf example's API. Every Lathe block is typechecked against that generated client in CI; the hey-api blocks are illustrative.

1. Run lathe init

npx lathe init
hey-apiLathe
input / input.pathinput (a URL becomes source, fetched by lathe pull)
output / output.pathoutput
@hey-api/client-fetch (-next, -nuxt)the default client: 'pyreon' — fetch-based, and the only client that reaches iOS/Android
@hey-api/client-axiosclient: 'axios'
@hey-api/typescript, @hey-api/schemasplugins: ['schemas'] — types and runtime schemas together
@hey-api/sdkplugins: ['client'] — one endpoint per operation
@tanstack/*-queryplugins: ['queries'] — @pyreon/query hooks
zodvalidator: 'zod'
output.format, output.lintrun your formatter after lathe generate in the script

A plugin with no Lathe equivalent is listed by name; nothing is dropped silently. When the old output directory still holds hey-api's files, Lathe writes to ./src/gen beside it.

2. Calling an operation

// hey-api SDK
import { getBook } from './client'

const { data, error } = await getBook({ path: { bookId: '1' } })

Lathe endpoints resolve to the validated body and REJECT on a non-2xx status (with a typed status and body), so there is no { data, error } pair to unpack. Path parameters are params, query parameters query.

// @check
import { getBook } from './gen'

const book = await getBook({ params: { bookId: '1' } })
book.title

3. Queries and mutations

// hey-api + @tanstack/react-query
const books = useQuery(listBooksOptions())
const create = useMutation(createBookMutation())
create.mutate({ body: { title: 'Dune' } })

Lathe generates the hook itself rather than an options factory to spread into one:

// @check
import { signal } from '@pyreon/reactivity'
import { useCreateBook, useGetBook, useListBooks } from './gen'

const books = useListBooks()
books.data()?.length

const bookId = signal('1')
// An accessor: a new id refetches.
const book = useGetBook(() => ({ params: { bookId: bookId() } }))
book.data()?.title

const create = useCreateBook()
create.mutate({ json: { title: 'Dune' } })

A mutation invalidates the queries it can change (listBooks aftercreateBook) unless you pass invalidates. The query key of any read is onkeys — keys.books.listBooks.all — for manual invalidation.

4. Base URL, headers and interceptors

// hey-api
import { client } from './client/client.gen'

client.setConfig({ baseUrl: 'https://api.example.com', auth: () => getToken() })
client.interceptors.request.use((request) => {
  request.headers.set('x-request-id', crypto.randomUUID())
  return request
})
// @check
import type { HttpMiddleware } from '@pyreon/http'
import { configureApi } from './gen'

const requestId: HttpMiddleware = (req, next) => {
  req.headers.set('x-request-id', crypto.randomUUID())
  return next(req)
}

let token = ''
configureApi({
  baseUrl: 'https://bookshelf.test/v1',
  headers: () => ({ Authorization: `Bearer ${token}` }),
  use: [requestId],
})
token = 'signed-in'

Each key is independent — setting headers keeps the base URL — and read on every request. With securitySchemes in the spec, the client also exportsauth.<scheme>(credential) helpers for use.

5. Mocks

hey-api does not generate mocks, so this is new: add mocks (and faker for factories) to plugins.

// @check
import { installMocks, mockCalls, mockOperation } from './gen/dev'

installMocks()
mockOperation('getBook', { status: 404, json: { message: 'no such book' } })
mockCalls.length

6. Validation

hey-api's validator: true on the SDK parses responses with the generated schemas. Lathe validates every response by default; responseValidation('strict' | 'warn' | 'off') in the config, or configureApi({ validate }) at runtime, changes that.

See also: Lathe, coming from orval,kubb, openapi-fetch.

Lathe — coming from hey-api