Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

1 watching

Forks

Repository files navigation

@zyno-io/openapi-client-codegen

A TypeScript library that wraps @hey-api/openapi-ts to provide a batteries-included workflow for generating and consuming OpenAPI clients. It adds runtime client configuration, error handling, request middleware, and file upload support on top of the generated SDK.

Table of Contents

Why not just use @hey-api/openapi-ts directly?

@hey-api/openapi-ts is a powerful codegen tool, but it only generates code — it doesn't provide runtime utilities for configuring the generated client in an application. This library fills that gap:

  • Client configuration — configureOpenApiClient() sets up error handling, dynamic headers, and request middleware in one call.
  • Structured errors — Responses are wrapped in OpenApiError with access to the request, response, and parsed body, rather than raw fetch errors.
  • Request middleware — A wrapper function lets you intercept every request for logging, auth token injection, retry logic, etc.
  • File uploads — Upload browser Blob/File values or local files in React Native and Expo, including arrays of files.
  • CLI and watch mode — A CLI tool and programmatic API for generating clients, with file watching for automatic regeneration during development.

Installation

npm install @zyno-io/openapi-client-codegen
# or
yarn add @zyno-io/openapi-client-codegen

Generating a Client

CLI

# Generate from a single spec
npx generate-openapi-client ./api.openapi.yaml ./src/generated

# Generate from config file (openapi-specs.json)
npx generate-openapi-client

# Watch mode — regenerates on file changes
npx generate-openapi-client -w ./api.openapi.yaml ./src/generated
npx generate-openapi-client -w  # watch all configured specs

Config file

Create openapi-specs.json in your project root:

{
    "./specs/api.yaml": "./src/generated/api",
    "./specs/admin.yaml": {
        "path": "./src/generated/admin",
        "prefix": "Admin"
    }
}

The prefix option prepends to generated SDK class names (e.g. AdminUsersApi instead of UsersApi).

To generate a client with only a subset of operations (and their referenced schemas), add operations:

{
    "./specs/api.yaml": {
        "path": "./src/generated/api",
        "operations": ["listPets", "getPet"]
    }
}

This filters the spec to only include the named operations (matched by operationId) and any schemas they reference, including transitive references. This is useful for keeping generated clients small when you only need a few endpoints from a large API.

For development, create openapi-specs.dev.json to redirect spec paths to local files without modifying the main config:

{
    "./specs/api.yaml": "../backend/api.openapi.yaml"
}

The keys match the paths in openapi-specs.json; the values point to where the spec actually lives during development (e.g. a file generated by a locally running backend). When using watch mode, the generator watches the dev path instead of the committed path. It only copies the dev spec back to the committed path when their contents differ, so spec changes still show up in git diff without unnecessary file writes.

Each generated SDK directory contains a .openapi-client-codegen.hash file. It records the YAML's SHA-256 hash and output-affecting generator options; if it matches on a later run, generation is skipped.

To override the operations filter during development, use the object form with operations. This takes precedence over any operations defined in openapi-specs.json:

{
    "./specs/api.yaml": {
        "src": "../backend/api.openapi.yaml",
        "operations": ["listPets", "getPet"]
    }
}

Programmatic API

import {
    generateOpenapiClient,
    generateConfiguredOpenapiClients,
    createWatchfulOpenapiClientGenerator
} from '@zyno-io/openapi-client-codegen/generator';

// Generate a single client
await generateOpenapiClient('./api.yaml', './src/generated');

// Generate all clients from openapi-specs.json
await generateConfiguredOpenapiClients();

// Watch a single spec and regenerate on changes
const watcher = createWatchfulOpenapiClientGenerator('./api.yaml', './src/generated');
// watcher.generate() — manually trigger
// watcher.close() — stop watching

Configuring the Generated Client

The generated SDK exports a client instance. Pass it to configureOpenApiClient() to set up error handling, headers, and middleware:

import { configureOpenApiClient } from '@zyno-io/openapi-client-codegen';
import { client } from './generated/client.gen';

configureOpenApiClient(client, {
    headers: {
        Authorization: `Bearer ${token}`
    },
    onError: (err, options) => {
        if (err instanceof OpenApiError && err.response?.status === 401) {
            redirectToLogin();
            return null; // the request promise will never settle
        }
    }
});

Options

headers

Static headers, or a (sync/async) function that returns headers per-request:

// Static
configureOpenApiClient(client, {
    headers: { 'X-API-Key': 'abc123' }
});

// Dynamic
configureOpenApiClient(client, {
    headers: async request => ({
        Authorization: `Bearer ${await getToken()}`
    })
});

Set a header to null to remove it from the request.

onError

Called when the server returns an error response. Receives an OpenApiError and the request options:

configureOpenApiClient(client, {
    onError: (err, options) => {
        // Throw a different error
        if (err.response?.status === 403) {
            throw new ForbiddenError(err.message);
        }

        // Return null to silently discard the error — the request promise
        // will never resolve or reject, so the caller hangs and does nothing.
        // Useful when you're redirecting the user away from the page entirely.
        if (err.response?.status === 401) {
            redirectToLogin();
            return null;
        }

        // Return void to throw the original OpenApiError
    }
});

wrapper

A middleware function that wraps every request. Receives the request options and the original request function, and must return the result. Useful for logging, retry logic, or request queuing:

configureOpenApiClient(client, {
    wrapper: async (options, request) => {
        const start = performance.now();
        try {
            const result = await request(options);
            console.log(`${options.method} ${options.url} — ${Math.round(performance.now() - start)}ms`);
            return result;
        } catch (err) {
            console.error(`${options.method} ${options.url} failed`, err);
            throw err;
        }
    }
});

File Uploads

First, configure the generated client with configureOpenApiClient(). This enables automatic file upload handling for SDK calls.

For Deepkit endpoints with UploadedFile parameters, put files in top-level body fields. The runtime sends those fields as multipart file parts and puts the remaining body fields in the _payload JSON part expected by Deepkit. It handles the multipart Content-Type and boundary for you.

Browser

Pass a native File or Blob directly:

import { ReportsApi } from './generated/sdk.gen';

// pdfBlob is an existing Blob, for example from a downloaded or generated PDF.
const pdfFile = new File([pdfBlob], 'report.pdf', { type: 'application/pdf' });
await ReportsApi.postReportsUpload({
    body: {
        title: 'Q4 Report',
        pdf: pdfFile
    }
});

The SDK class, method, and field names in these examples depend on your API spec.

Expo and React Native

Wrap a local file URI in ReactNativeFileUploadRequest. When using expo/fetch, also provide a bytes callback so Expo can read the file's contents. This example uses File.bytes() from expo-file-system:

import { ReactNativeFileUploadRequest } from '@zyno-io/openapi-client-codegen';
import { File as ExpoFile } from 'expo-file-system';
import { ProfileApi } from './generated/sdk.gen';

// photo.uri is a local file URI, for example from an image picker.
await ProfileApi.postProfileUploadPhoto({
    body: {
        photo: new ReactNativeFileUploadRequest({
            uri: photo.uri,
            name: 'photo.jpg',
            type: 'image/jpeg',
            bytes: () => new ExpoFile(photo.uri).bytes()
        })
    }
});

With React Native's native networking, you can omit bytes; it reads the file from uri. With expo/fetch, a missing bytes callback causes an error that identifies the file. Set name to the filename the server should receive and type to the file's MIME type, such as image/jpeg.

Multiple files

For an array upload field, pass an array of files or upload helpers:

import { MessagesApi } from './generated/sdk.gen';

// firstFile and secondFile are File/Blob values or upload helpers.
await MessagesApi.sendMessage({
    body: {
        textContent: 'Here are the photos',
        attachments: [firstFile, secondFile]
    }
});

Each file is sent under the same field name (attachments here), in array order. A file array must contain only files or upload helpers; mixing them with strings, null, or other values throws a TypeError.

If the endpoint accepts both JSON and multipart, a request without file values uses JSON. Empty arrays such as attachments: [] stay in the JSON body.

Generated types

Supported multipart file fields accept Blob | File | FileUploadValue; array fields accept an array of those values. FileUploadValue is an exported type covering FileUploadRequest and ReactNativeFileUploadRequest. Fields remain optional or nullable when declared that way in the spec.

When an endpoint accepts both JSON and multipart, the generated body type uses the multipart schema so you can pass file fields even if the JSON schema omits them. Automatic helper typing applies to top-level file fields; shared response schemas, external references, and ambiguous composed schemas keep their original types.

After upgrading this package, rerun client generation to refresh the upload types. The generator invalidates older cached output even if your spec has not changed.

Response Utilities

import { dataFrom, dataFromAsync, type OpenApiDataType } from '@zyno-io/openapi-client-codegen';

// Extract .data from a response
const response = await PetsApi.listPets();
const pets = dataFrom(response);

// Extract .data from a promise
const pets = await dataFromAsync(PetsApi.listPets());

// Extract the data type from a response type
type Pet = OpenApiDataType<Awaited<ReturnType<typeof PetsApi.getPet>>>;

Error Handling

configureOpenApiClient sets throwOnError: true on the client. Failed requests throw an OpenApiError:

import { OpenApiError } from '@zyno-io/openapi-client-codegen';

try {
    await PetsApi.getPet({ path: { petId: '123' } });
} catch (err) {
    if (err instanceof OpenApiError) {
        console.error(err.message); // "Not Found (404)"
        console.error(err.response); // Response object
        console.error(err.body); // Parsed response body
        console.error(err.request); // Request object
    }
}

License

MIT

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages