openapi-fetch-gen – Generate TypeScript API client from OpenAPI TypeScript interface definitions created by openapi-typescript
moznion / openapi-fetch-gen Generate TypeScript API client from OpenAPI TypeScript interface definitions created by openapi-typescript (https://github.com/openapi-ts/openapi-typescript). openapi-fetch-gen Generate TypeScript API client from OpenAPI TypeScript interface definitions created by openapi-typescript. This tool takes TypeScript interface definitions generated by openapi-typescript and creates a fully typed API client using openapi-fetch. How It Works Parse the TypeScript schema file generated by openapi-typescript Extract all API endpoints, their HTTP methods, and parameter structures from the schema Generate typed wrapper functions for each endpoint Export a fully-typed client that provides A base client instance created with createClient from openapi-fetch Individual typed functions for each API endpoint Type helpers for parameters and responses Installation npm install --save-dev @moznion/openapi-fetch-gen Usage CLI npx openapi-fetch-gen --input ./schema.d.ts --output ./client.ts Options: -V, --version output the version number -i, --input path to input OpenAPI TypeScript definition file -o, --output path to output generated client file (default: "./client.ts") --default-headers header names so that the generated client includes the default HTTP headers across all endpoints… View on GitHub Background/Motivation The goal of openapi-fetch-gen is to automatically generate a fully-typed TypeScript API client from the .d.ts schema generated by openapi-ts/openapi-typescript. openapi-ts/openapi-typescript is a powerful tool for generating TypeScript schema definitions from OpenAPI 3 specifications. The same organization also provides openapi-fetch, a type-safe fetch client library designed to work seamlessly with these generated schemas. Although very useful, one notable hurdle with openapi-fetch is that developers must manually implement client code for each API endpoint. How This Tool Works For instance, suppose you have an OpenAPI 3 YAML definition like the following: schema.yaml (a bit long, but imagine a typical REST API definition) openapi: 3.0.3 info: title: Fictional Library User Management Service API version: 1.0.0 description: | A RESTful API for managing library user records and their loan information. servers: - url: https://api.fictionallibrary.example.com/v1 description: Production server paths: /users/{userId}: parameters: - $ref: '#/components/parameters/userId' - name: Authorization in: header required: true schema: type: string description: Authorization Header - name: Application-Version in: header required: true schema: type: string description: Application version - name: Something-Id in: header required: true schema: type: string description: Identifier of something get: summary: Get user details description: Retrieve detailed information for a specific user. responses: '200': description: User details content: application/json: schema: $ref: '#/components/schemas/User' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' put: summary: Replace user description: Replace a user's entire record. requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UserUpdate' responses: '200': description: Updated user record content: application/json: schema: $ref: '#/components/schemas/User' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' patch: summary: Update user fields description: Partially update a user's information. requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UserPatch' responses: '200': description: Updated user record content: application/json: schema: $ref: '#/components/schemas/User' '400': $ref: '#/components/responses/BadReques
moznion
/
openapi-fetch-gen
Generate TypeScript API client from OpenAPI TypeScript interface definitions created by openapi-typescript (https://github.com/openapi-ts/openapi-typescript).
Generate TypeScript API client from OpenAPI TypeScript interface definitions created by openapi-typescript.
This tool takes TypeScript interface definitions generated by openapi-typescript
and creates a fully typed API client using openapi-fetch.
How It Works
- Parse the TypeScript schema file generated by
openapi-typescript
- Extract all API endpoints, their HTTP methods, and parameter structures from the schema
- Generate typed wrapper functions for each endpoint
- Export a fully-typed client that provides
- A base client instance created with
createClient
fromopenapi-fetch
- Individual typed functions for each API endpoint
- Type helpers for parameters and responses
- A base client instance created with
Installation
npm install --save-dev @moznion/openapi-fetch-gen
Usage
CLI
npx openapi-fetch-gen --input ./schema.d.ts --output ./client.ts
Options:
-V, --version output the version number
-i, --input path to input OpenAPI TypeScript definition file
-o, --output path to output generated client file (default: "./client.ts")
--default-headers header names so that the generated client includes the default HTTP headers across all endpoints
…Background/Motivation
The goal of openapi-fetch-gen is to automatically generate a fully-typed TypeScript API client from the .d.ts
schema generated by openapi-ts/openapi-typescript.
openapi-ts/openapi-typescript is a powerful tool for generating TypeScript schema definitions from OpenAPI 3 specifications. The same organization also provides openapi-fetch, a type-safe fetch client library designed to work seamlessly with these generated schemas. Although very useful, one notable hurdle with openapi-fetch is that developers must manually implement client code for each API endpoint.
How This Tool Works
For instance, suppose you have an OpenAPI 3 YAML definition like the following:
schema.yaml (a bit long, but imagine a typical REST API definition)
openapi: 3.0.3
info:
title: Fictional Library User Management Service API
version: 1.0.0
description: |
A RESTful API for managing library user records and their loan information.
servers:
- url: https://api.fictionallibrary.example.com/v1
description: Production server
paths:
/users/{userId}:
parameters:
- $ref: '#/components/parameters/userId'
- name: Authorization
in: header
required: true
schema:
type: string
description: Authorization Header
- name: Application-Version
in: header
required: true
schema:
type: string
description: Application version
- name: Something-Id
in: header
required: true
schema:
type: string
description: Identifier of something
get:
summary: Get user details
description: Retrieve detailed information for a specific user.
responses:
'200':
description: User details
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
put:
summary: Replace user
description: Replace a user's entire record.
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UserUpdate'
responses:
'200':
description: Updated user record
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
patch:
summary: Update user fields
description: Partially update a user's information.
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UserPatch'
responses:
'200':
description: Updated user record
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
delete:
summary: Delete user
description: Soft-delete a user record.
responses:
'204':
description: User deleted (no content)
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
components:
parameters:
userId:
name: userId
in: path
required: true
description: Unique user identifier (UUID)
schema:
type: string
format: uuid
responses:
BadRequest:
description: Bad request due to invalid input
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Unauthorized:
description: Authentication required or failed
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Forbidden:
description: Insufficient permissions to access resource
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
NotFound:
description: Resource not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Conflict:
description: Conflict with current state of the resource
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
schemas:
Error:
type: object
properties:
code:
type: integer
description: HTTP status code
message:
type: string
description: Error message detailing the cause
required:
- code
- message
User:
type: object
properties:
id:
type: string
format: uuid
name:
type: string
email:
type: string
format: email
membershipType:
type: string
enum: [REGULAR, PREMIUM, STUDENT]
registeredAt:
type: string
format: date-time
address:
$ref: '#/components/schemas/Address'
required:
- id
- name
- email
- membershipType
- registeredAt
Address:
type: object
properties:
postalCode:
type: string
street:
type: string
city:
type: string
country:
type: string
required:
- street
- city
- country
UserCreate:
type: object
properties:
name:
type: string
email:
type: string
format: email
membershipType:
type: string
enum: [REGULAR, PREMIUM, STUDENT]
address:
$ref: '#/components/schemas/Address'
required:
- name
- email
- membershipType
UserUpdate:
allOf:
- $ref: '#/components/schemas/UserCreate'
- type: object
properties:
id:
type: string
format: uuid
required:
- id
UserPatch:
type: object
description: Schema for partial updates – include only fields to change
properties:
name:
type: string
email:
type: string
format: email
membershipType:
type: string
enum: [REGULAR, PREMIUM, STUDENT]
address:
$ref: '#/components/schemas/Address'
Next, you execute:
openapi-typescript --output schema.d.ts ./schema.yaml
This generates a TypeScript interface definition file named schema.d.ts
:
schema.d.ts
/**
* This file was auto-generated by openapi-typescript.
* Do not make direct changes to the file.
*/
export interface paths {
"/users/{userId}": {
parameters: {
query?: never;
header: {
/** @description Authorization Header */
Authorization: string;
/** @description Application version */
"Application-Version": string;
/** @description Identifier of something */
"Something-Id": string;
};
path: {
/** @description Unique user identifier (UUID) */
userId: components["parameters"]["userId"];
};
cookie?: never;
};
/**
* Get user details
* @description Retrieve detailed information for a specific user.
*/
get: {
parameters: {
query?: never;
header: {
/** @description Authorization Header */
Authorization: string;
/** @description Application version */
"Application-Version": string;
/** @description Identifier of something */
"Something-Id": string;
};
path: {
/** @description Unique user identifier (UUID) */
userId: components["parameters"]["userId"];
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description User details */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["User"];
};
};
400: components["responses"]["BadRequest"];
401: components["responses"]["Unauthorized"];
403: components["responses"]["Forbidden"];
404: components["responses"]["NotFound"];
};
};
/**
* Replace user
* @description Replace a user's entire record.
*/
put: {
parameters: {
query?: never;
header: {
/** @description Authorization Header */
Authorization: string;
/** @description Application version */
"Application-Version": string;
/** @description Identifier of something */
"Something-Id": string;
};
path: {
/** @description Unique user identifier (UUID) */
userId: components["parameters"]["userId"];
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["UserUpdate"];
};
};
responses: {
/** @description Updated user record */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["User"];
};
};
400: components["responses"]["BadRequest"];
401: components["responses"]["Unauthorized"];
403: components["responses"]["Forbidden"];
404: components["responses"]["NotFound"];
};
};
post?: never;
/**
* Delete user
* @description Soft-delete a user record.
*/
delete: {
parameters: {
query?: never;
header: {
/** @description Authorization Header */
Authorization: string;
/** @description Application version */
"Application-Version": string;
/** @description Identifier of something */
"Something-Id": string;
};
path: {
/** @description Unique user identifier (UUID) */
userId: components["parameters"]["userId"];
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description User deleted (no content) */
204: {
headers: {
[name: string]: unknown;
};
content?: never;
};
400: components["responses"]["BadRequest"];
401: components["responses"]["Unauthorized"];
403: components["responses"]["Forbidden"];
404: components["responses"]["NotFound"];
};
};
options?: never;
head?: never;
/**
* Update user fields
* @description Partially update a user's information.
*/
patch: {
parameters: {
query?: never;
header: {
/** @description Authorization Header */
Authorization: string;
/** @description Application version */
"Application-Version": string;
/** @description Identifier of something */
"Something-Id": string;
};
path: {
/** @description Unique user identifier (UUID) */
userId: components["parameters"]["userId"];
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["UserPatch"];
};
};
responses: {
/** @description Updated user record */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["User"];
};
};
400: components["responses"]["BadRequest"];
401: components["responses"]["Unauthorized"];
403: components["responses"]["Forbidden"];
404: components["responses"]["NotFound"];
};
};
trace?: never;
};
}
export type webhooks = Record<string, never>;
export interface components {
schemas: {
Error: {
/** @description HTTP status code */
code: number;
/** @description Error message detailing the cause */
message: string;
};
User: {
/** Format: uuid */
id: string;
name: string;
/** Format: email */
email: string;
/** @enum {string} */
membershipType: "REGULAR" | "PREMIUM" | "STUDENT";
/** Format: date-time */
registeredAt: string;
address?: components["schemas"]["Address"];
};
Address: {
postalCode?: string;
street: string;
city: string;
country: string;
};
UserCreate: {
name: string;
/** Format: email */
email: string;
/** @enum {string} */
membershipType: "REGULAR" | "PREMIUM" | "STUDENT";
address?: components["schemas"]["Address"];
};
UserUpdate: components["schemas"]["UserCreate"] & {
/** Format: uuid */
id: string;
};
/** @description Schema for partial updates – include only fields to change */
UserPatch: {
name?: string;
/** Format: email */
email?: string;
/** @enum {string} */
membershipType?: "REGULAR" | "PREMIUM" | "STUDENT";
address?: components["schemas"]["Address"];
};
};
responses: {
/** @description Bad request due to invalid input */
BadRequest: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Error"];
};
};
/** @description Authentication required or failed */
Unauthorized: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Error"];
};
};
/** @description Insufficient permissions to access resource */
Forbidden: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Error"];
};
};
/** @description Resource not found */
NotFound: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Error"];
};
};
/** @description Conflict with current state of the resource */
Conflict: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Error"];
};
};
};
parameters: {
/** @description Unique user identifier (UUID) */
userId: string;
};
requestBodies: never;
headers: never;
pathItems: never;
}
export type $defs = Record<string, never>;
export type operations = Record<string, never>;
Previously, you would have needed to manually implement the API client for each endpoint using openapi-fetch. Now, with openapi-fetch-gen, the process is significantly simplified:
Just execute:
openapi-fetch-gen --input ./schema.d.ts --output ./generated_client.ts
This automatically generates the following fully-typed API client code:
import createClient, { type ClientOptions } from "openapi-fetch";
import type { paths } from "./schema"; // generated by openapi-typescript
export class Client {
private readonly client;
constructor(clientOptions: ClientOptions) {
this.client = createClient<paths>(clientOptions);
}
...
/**
* Replace user
*/
async putUsersUserid(
params: {
header: {
Authorization: string;
"Application-Version": string;
"Something-Id": string;
};
path: { userId: string };
},
body: {
name: string;
email: string;
membershipType: "REGULAR" | "PREMIUM" | "STUDENT";
address?: {
postalCode?: string;
street: string;
city: string;
country: string;
};
} & { id: string },
) {
return await this.client.PUT("/users/{userId}", {
params,
body,
});
}
...
}
FYI: complete example of generated_client.ts
// THIS FILE IS AUTO-GENERATED BY openapi-fetch-gen.
// DO NOT EDIT THIS FILE MANUALLY.
// See Also: https://github.com/moznion/openapi-fetch-gen
import createClient, { type ClientOptions } from "openapi-fetch";
import type { paths } from "./schema"; // generated by openapi-typescript
export class Client {
private readonly client;
constructor(clientOptions: ClientOptions) {
this.client = createClient<paths>(clientOptions);
}
/**
* Get user details
*/
async getUsersUserid(params: {
header: {
Authorization: string;
"Application-Version": string;
"Something-Id": string;
};
path: { userId: string };
}) {
return await this.client.GET("/users/{userId}", {
params,
});
}
/**
* Replace user
*/
async putUsersUserid(
params: {
header: {
Authorization: string;
"Application-Version": string;
"Something-Id": string;
};
path: { userId: string };
},
body: {
name: string;
email: string;
membershipType: "REGULAR" | "PREMIUM" | "STUDENT";
address?: {
postalCode?: string;
street: string;
city: string;
country: string;
};
} & { id: string },
) {
return await this.client.PUT("/users/{userId}", {
params,
body,
});
}
/**
* Delete user
*/
async deleteUsersUserid(params: {
header: {
Authorization: string;
"Application-Version": string;
"Something-Id": string;
};
path: { userId: string };
}) {
return await this.client.DELETE("/users/{userId}", {
params,
});
}
/**
* Update user fields
*/
async patchUsersUserid(
params: {
header: {
Authorization: string;
"Application-Version": string;
"Something-Id": string;
};
path: { userId: string };
},
body: {
name?: string;
email?: string;
membershipType?: "REGULAR" | "PREMIUM" | "STUDENT";
address?: {
postalCode?: string;
street: string;
city: string;
country: string;
};
},
) {
return await this.client.PATCH("/users/{userId}", {
params,
body,
});
}
}
That's all there is to it! ✨
Under the Hood
This tool analyzes the provided .d.ts
file using the TypeScript Compiler API via the dsherret/ts-morph library and generates the corresponding TypeScript API client code based on the analyzed structure. It's clearly simple :)
Wrap-up
In summary, openapi-fetch-gen
automates the generation of a TypeScript API client layer directly from the openapi-typescript schema definitions, removing the need for tedious manual implementation.
Additionally, this tool supports a "Default HTTP Headers" feature that simplifies type definitions for client users. The original implementation required explicitly passing headers (e.g., Authorization
) in many methods. To simplify this, openapi-fetch-gen uses a tiny type-level trick to handle these headers more gracefully. If you're interested, please refer to the Default HTTP Headers section.