Generate TypeScript HTTP clients from OpenAPI 3.0 / 3.1 specifications. The generated code has zero runtime dependencies. Full type safety. Bring your own HTTP client.
- Full OpenAPI 3.0 and 3.1 specification support with automatic version detection
- End-to-end type safety — requests, responses, and errors are fully typed
- HTTP-client agnostic — adapter pattern lets you plug in fetch, axios, or anything else
- Error types with per-status-code narrowing and type guards
- File and binary upload/download with stream handling
- Flexible method naming strategies (path-based, operationId, operationId-with-fallback)
- CLI and programmatic API
Install:
npm install -D genocGenerate:
genoc ./path/to/spec.yaml --output-dir ./src/apiThis creates two files in ./src/api:
contracts.ts— Type definitions, error classes, and helper typesclient.ts— Typed client withcreateClient(requester)factory
The generated client requires a Requester implementation — a function that
performs the actual HTTP call and returns the result. This is the type your
implementation must satisfy:
type Requester = <TResponse>(
method: string,
path: string,
options: {
query?: Record<string, unknown>;
body?: unknown;
headers?: Record<string, string>;
expectStream?: true;
}
) => Promise<TResponse | StreamResponse | ErrorResponse>;import { createClient } from './client.js';
import { ApiError, RequesterFailError, ErrorResponse } from './contracts.js';
const baseUrl = 'https://api.example.com';
const requester: Requester = async (method, path, options) => {
const url = new URL(path, baseUrl);
if (options.query) {
Object.entries(options.query).forEach(([key, value]) => {
url.searchParams.set(key, String(value));
});
}
const response = await fetch(url, {
method,
headers: {
'Content-Type': 'application/json',
...options.headers,
},
body: options.body ? JSON.stringify(options.body) : undefined,
});
if (!response.ok) {
return new ErrorResponse(
response.status,
await response.json(),
response.headers,
response.statusText
);
}
return response.json();
};
const client = createClient(requester);
// Typed call — response type is inferred from the spec
const pets = await client.getPets({ limit: 10 });See Binary / File Responses for handling expectStream: true.
When your spec defines binary responses (e.g. format: binary,
application/octet-stream, image/*), the generated client sends
expectStream: true in options. Your Requester should return a
StreamResponse in that case:
import { StreamResponse } from './contracts.js';
// Inside your Requester implementation:
if (options.expectStream === true) {
return new StreamResponse(
response.body as ReadableStream<Uint8Array>,
getFilename(response.headers), // extract from Content-Disposition
response.headers
);
}StreamResponse is a simple container:
class StreamResponse {
data: ReadableStream<Uint8Array>;
filename?: string;
headers: Record<string, string>;
}The generated contracts.ts includes helper functions for constructing
responses in your Requester implementation:
streamResponse(data, filename?, headers?)— Creates aStreamResponseinstanceerrorResponse(status, data, headers?, message?)— Creates anErrorResponseinstance
These are convenience wrappers around the StreamResponse and ErrorResponse
constructors.
genoc <spec> [flags]<spec> — Path or URL to an OpenAPI 3.0 / 3.1 spec (JSON or YAML).
| Flag | Default | Description |
|---|---|---|
--output-dir |
(required) | Output directory for generated files |
--method-name-strategy |
path-based |
Method naming strategy |
--spec-version |
auto-detect | Override version detection ("3.0" or "3.1") |
--strict-version |
true |
Warn if --spec-version mismatches detected version |
-
path-based(default) — HTTP method + path segments in PascalCase.GET /pets→getPets,GET /api/v1/products→getApiV1Products -
operationId— Use theoperationIdfield from the spec.GET /pets→findPets(ifoperationIdis"findPets") -
operationId-with-fallback— UseoperationIdif present, otherwise fall back to path-based naming.
import { generateClient } from 'genoc';
await generateClient({
input: './openapi.yaml',
outputDir: './src/api',
methodNameStrategy: 'path-based',
specVersion: '3.1',
strictVersion: true,
});The generated client throws typed errors. Each method carries its own error
union, and isDefinedError narrows a caught error to that union:
ApiError<TStatus, TData>— Error for a specific status code defined in the specUnspecifiedApiError— Error for a status code not defined in the specRequesterFailError— Wraps unexpected failures in yourRequesterisDefinedError(err, client.method)— Type guard that narrows to the method's defined error union
import { UnspecifiedApiError, RequesterFailError } from './contracts.js';
import { isDefinedError } from './client.js';
try {
const result = await client.getPets();
} catch (error) {
if (isDefinedError(error, client.getPets)) {
// error is narrowed to GetPetsErrors (ApiError<400, ...> | ApiError<500, ...>)
if (error.status === 400) {
console.error('Bad request:', error.data);
}
}
if (error instanceof UnspecifiedApiError) {
console.error('Unexpected status:', error.status, error.data);
}
if (error instanceof RequesterFailError) {
console.error('Requester failed:', error.cause);
}
}Check the detailed feature support tables to see if your OpenAPI spec features are covered:
- OpenAPI 3.0 Support — Data types, schema keywords, parameters, request bodies, file uploads, responses, error handling,
$refresolution, components, security schemes, servers, and path operations. - OpenAPI 3.1 Support — All 3.0 features plus type arrays,
$refsiblings, webhooks, JSON Schema 2020-12 alignment, and a 3.0 → 3.1 diff.
- Node.js >= 18
- OpenAPI 3.0.x or 3.1.x specification (JSON or YAML, file path or URL)
MIT — Copyright © Andrey Kiselev