Skip to content
· 1 min read·
TypeScriptOpenAPIGitHub ActionsDeveloper ExperiencePHP

Auto-generating a TypeScript API Client from OpenAPI with @hey-api/openapi-ts

How I replaced hand-maintained API types with an auto-generated, fully typed TypeScript client — published to a private registry and rebuilt on every API spec change via GitHub Actions.

One of the most common sources of bugs in full-stack apps is the frontend and backend diverging silently. You rename a field on the API, forget to update the TypeScript interface, and two weeks later you get a production bug report.

At work, we built a new PHP/Laminas backend alongside a Nuxt 3 frontend. From day one, I wanted to make the contract between them machine-enforced.

The setup

The backend uses OpenAPI attributes on every handler and DTO:

<?php

#[OA\Get(
    path: '/customers',
    operationId: 'getCustomers',
    responses: [
        new OA\Response(response: 200, description: 'List of customers', content: new OA\JsonContent(ref: CustomerListResponse::class))
    ]
)]
class GetCustomersHandler
{
    // ...
}

A PHPStan rule enforces that every handler has OpenAPI documentation — missing it fails CI.

From those attributes, we generate an openapi.json spec file. That spec is the single source of truth.

Generating the TypeScript client

@hey-api/openapi-ts reads the OpenAPI spec and generates:

  • Typed request/response interfaces
  • A fully typed client (we use the fetch client)
  • SDK methods for every operation
npx @hey-api/openapi-ts \
  --input ./openapi.json \
  --output ./src/client \
  --client @hey-api/client-fetch

The generated client looks like this on the consuming side:

import { getCustomers } from '@acme/api-client'

const { data, error } = await getCustomers({ query: { page: 1 } })
// data is fully typed — CustomerListResponse

Publishing to a private registry

We publish the generated client as a private npm package. The frontend installs it like any other dependency: npm install @acme/api-client.

Keeping it up to date: GitHub Actions

The real value comes from automation. A GitHub Actions workflow:

  1. Runs on every push that touches PHP files or openapi.json
  2. Regenerates the client
  3. Bumps the patch version
  4. Publishes the new package to the registry

The frontend's Renovate config picks up the new version automatically and opens a PR.

The result

The gap between API contract and frontend types is now zero — they're the same file. Schema drift is impossible without a failing CI job. And new developers don't need to figure out the API shape by reading PHP code; they just look at the TypeScript types.

This pattern works equally well with any typed backend that can emit an OpenAPI spec.

Newsletter

I write about PHP, Vue, TypeScript, and developer experience. No spam, unsubscribe anytime.

Copyright © 2026. All rights reserved.