pyreon

Lathe and orval do the same job — an OpenAPI document in, a typed client out — so most of a migration is configuration. lathe init readsorval.config.ts and does that part; this page covers the code that calls the client.

The examples use the Bookshelf example's API (listBooks, getBook, createBook). Every Lathe block below is typechecked against that generated client in CI; the orval blocks are illustrative.

1. Run lathe init

npx lathe init

It finds orval.config.ts (it is read as text — orval does not need to stay installed), writes a lathe section into pyreon.config.ts, addslathe:generate / lathe:check scripts, prints the packages to install and runs the first generate. --yes takes every default for CI; --dry-run shows what it would write.

orvalLathe
input / input.targetinput (a URL becomes source, fetched by lathe pull)
output.targetoutput — a directory; Lathe writes one module per tag
output.client: 'react-query' (vue-/svelte-/solid-query, swr)plugins: [..., 'queries'] — @pyreon/query hooks
output.client: 'axios' / httpClient: 'axios'client: 'axios'
output.client: 'zod'validator: 'zod'
output.httpClient: 'fetch'client: 'fetch' (or keep the default pyreon, the only one that reaches iOS/Android)
output.mock: trueplugins: [..., 'mocks', 'faker']
output.baseUrl (string)baseUrl
output.mode, output.schemas, output.cleanbuilt in: one module per tag, schemas/, stale files pruned
output.override.mutatorruntime middleware — see Auth
hooks, prettierchain your formatter after lathe generate in the script

Several APIs in one orval.config.ts become projects. Anything init cannot map is listed by name with what to do instead. If your old output directory still holds orval's files, Lathe writes to ./src/gen beside it so the app keeps working until the imports move.

2. Queries

// orval + react-query
import { useGetBook, useListBooks } from './api/books'

const books = useListBooks()
const book = useGetBook(bookId)
books.data?.map((b) => b.title)

Lathe hooks take their arguments as an accessor, so a signal inside them refetches when it changes, and return signals — call them to read.

// @check
import { For, type VNodeChild } from '@pyreon/core'
import { signal } from '@pyreon/reactivity'
import { useGetBook, useListBooks } from './gen'

export function Books(): VNodeChild {
  const books = useListBooks()
  return (
    <ul>
      <For each={() => books.data() ?? []} by={(b) => b.id}>
        {(b) => <li>{b.title}</li>}
      </For>
    </ul>
  )
}

const bookId = signal<string | undefined>(undefined)
// `undefined` holds the request until there is an id to fetch.
const book = useGetBook(() => (bookId() ? { params: { bookId: bookId() as string } } : undefined))
book.data()?.title

Query options (orval's query: { … } override, or a per-call options object) are the hook's second accessor: useListBooks(() => ({ staleTime: 60_000 })).

3. Mutations

// orval + react-query
const create = useCreateBook()
create.mutate({ data: { title: 'Dune' } })
// @check
import { useCreateBook } from './gen'

const create = useCreateBook({ onSuccess: (book) => console.log(book.id) })
create.mutate({ json: { title: 'Dune' } })

The body key names the encoding: json here, form / multipart / bodyfor the others. A mutation already invalidates the queries it can change (listBooks after createBook); pass invalidates to change that.

4. Mocks: MSW → installMocks()

// orval + msw
import { setupServer } from 'msw/node'
import { getBooksMock } from './api/books.msw'

const server = setupServer(...getBooksMock())
server.listen()

Lathe's mocks are middleware on the client itself — no service worker, no server to start, identical in node and the browser.

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

installMocks()
// One test needs a failure:
const restore = mockOperation('listBooks', { status: 500, json: { message: 'down' } })
restore()
resetMocks()

createBook()-style fake-data factories come from the faker plugin, in the same ./gen/dev entry, which a page bundle can never import.

5. Auth and a custom instance

orval's mutator is a function every request goes through. In Lathe that is middleware plus headers, set at runtime:

// orval mutator
export const customInstance = <T>(config: AxiosRequestConfig): Promise<T> =>
  AXIOS_INSTANCE({ ...config, headers: { Authorization: `Bearer ${getToken()}` } }).then((r) => r.data)
// @check
import type { HttpMiddleware } from '@pyreon/http'
import { signal } from '@pyreon/reactivity'
import { configureApi } from './gen'

const token = signal('')

const retryOn401: HttpMiddleware = async (req, next) => {
  const res = await next(req)
  return res.status === 401 ? next(req) : res
}

configureApi({
  // Re-read on every request, so a new token applies immediately.
  headers: () => ({ Authorization: `Bearer ${token()}` }),
  use: [retryOn401],
})

When the spec declares securitySchemes, the client also exports a typed helper per scheme, named after it — configureApi({ use: [auth.bearerAuth(() => token())] })for a scheme called bearerAuth.

6. Base URL

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

const staging = true
configureApi({ baseUrl: staging ? 'https://staging.bookshelf.test/v1' : 'https://bookshelf.test/v1' })

The spec's servers[0].url (or the baseUrl config key) is the default;configureApi switches it per environment without regenerating.

7. Calling an endpoint directly

orval's plain functions become the generated endpoints — for a loader, a server route or a script, with no hooks involved:

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

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

What is different on purpose

  • Validation. Responses are checked against the generated schemas (responseValidation: 'strict' | 'warn' | 'off'), so a backend that drifts fails loudly at the boundary.

  • lathe check fails CI when the committed client is stale against the spec, and --fail-on-breaking when a spec change breaks your code.

  • Native. With target: 'multiplatform', the same spec produces modules that lower to Swift and Kotlin.

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

Lathe — coming from orval