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.
- Why not just use @hey-api/openapi-ts directly?
- Installation
- Generating a Client
- Configuring the Generated Client
- File Uploads
- Response Utilities
- Error Handling
@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
OpenApiErrorwith 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 — Browser
Blob/Filevalues and React Native upload helpers automatically convert to multipart/form-data. - CLI and watch mode — A CLI tool and programmatic API for generating clients, with file watching for automatic regeneration during development.
npm install @zyno-io/openapi-client-codegen
# or
yarn add @zyno-io/openapi-client-codegen# 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 specsCreate 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. After each successful generation, it automatically copies the dev spec back to the committed path so that spec changes show up in git diff and stay in sync with version control.
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"]
}
}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 watchingThe 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
}
}
});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.
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
}
});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;
}
}
});Generated multipart upload fields are represented as Blob | File | null. When a Deepkit backend exposes UploadedFile parameters, passing a Blob or File automatically converts the request to multipart/form-data, sends file fields as file parts, and moves non-file fields into the _payload JSON part expected by Deepkit:
import { ReactNativeFileUploadRequest } from '@zyno-io/openapi-client-codegen';
// Browser — pass a File or Blob directly
const pdfFile = new File([pdfBlob], 'report.pdf', { type: 'application/pdf' });
const result = dataFrom(
await ReportsApi.postReportsUpload({
body: {
title: 'Q4 Report',
pdf: pdfFile
}
})
);
// React Native — pass a file URI helper
await ProfileApi.postProfileUploadPhoto({
body: {
photo: new ReactNativeFileUploadRequest({
uri: photo.uri,
name: 'photo.jpg',
type: 'image/jpeg'
}) as unknown as Blob
}
});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>>>;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
}
}MIT