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> path to input OpenAPI TypeScript definition file
-o, --output <path> path to output generated client file (default: "./client.ts")
--default-headers <comma_separated_names_of_headers> header names so that the generated client includes the default HTTP headers across all endpoints
…This tool has already been published to the npm registry. You can install it by running:
npm install @moznion/openapi-fetch-gen
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.
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 clever type-level trick to handle these headers more gracefully. If you're interested, please refer to the Default HTTP Headers section.
Top comments (0)