DEV Community

moznion
moznion

Posted on

openapi-fetch-gen – Generate TypeScript API client from OpenAPI TypeScript interface definitions created by openapi-typescript

GitHub logo 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 Run Tests

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

  1. Parse the TypeScript schema file generated by openapi-typescript
  2. Extract all API endpoints, their HTTP methods, and parameter structures from the schema
  3. Generate typed wrapper functions for each endpoint
  4. 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
Enter fullscreen mode Exit fullscreen mode

Usage

CLI

npx openapi-fetch-gen --input ./schema.d.ts --output ./client.ts
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

Next, you execute:

openapi-typescript --output schema.d.ts ./schema.yaml
Enter fullscreen mode Exit fullscreen mode

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>;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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,
    });
  }
  ...
}
Enter fullscreen mode Exit fullscreen mode

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,
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

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)