Skip to content

typed-openapi

Install

sh
pnpm add typed-openapi

Directory Structure

.
├── openapi.yaml
└── package.json

openapi.yaml

yaml
openapi: 3.0.0
info:
  title: 顧客管理API
  version: '1.0.0'
  description: 顧客情報の作成、取得、更新、削除を行うためのAPI
servers:
  - url: 'https://api.example.com'
security:
  - OAuth2:
      - read
      - write
paths:
  /customers:
    get:
      summary: すべての顧客のリストを取得
      security:
        - OAuth2:
            - read
      responses:
        '200':
          description: 正常に取得しました
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Customer'
    post:
      summary: 新しい顧客を作成
      security:
        - OAuth2:
            - write
      requestBody:
        description: 顧客情報を含むリクエストボディ
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CustomerInput'
      responses:
        '201':
          description: 顧客が正常に作成されました
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Customer'
  /customers/{id}:
    get:
      summary: 特定の顧客の詳細を取得
      security:
        - OAuth2:
            - read
      parameters:
        - name: id
          in: path
          description: 顧客の一意な識別子
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: 顧客の詳細情報
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Customer'
        '404':
          description: 顧客が見つかりません
    put:
      summary: 特定の顧客情報を更新
      security:
        - OAuth2:
            - write
      parameters:
        - name: id
          in: path
          description: 顧客の一意な識別子
          required: true
          schema:
            type: integer
      requestBody:
        description: 更新する顧客情報
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CustomerInput'
      responses:
        '200':
          description: 顧客が正常に更新されました
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Customer'
        '404':
          description: 顧客が見つかりません
    delete:
      summary: 特定の顧客を削除
      security:
        - OAuth2:
            - write
      parameters:
        - name: id
          in: path
          description: 顧客の一意な識別子
          required: true
          schema:
            type: integer
      responses:
        '204':
          description: 顧客が正常に削除されました
        '404':
          description: 顧客が見つかりません
components:
  securitySchemes:
    OAuth2:
      type: oauth2
      flows:
        authorizationCode:
          authorizationUrl: 'https://example.com/oauth/authorize'
          tokenUrl: 'https://example.com/oauth/token'
          scopes:
            read: データの読み取り権限
            write: データの書き込み権限
  schemas:
    Customer:
      type: object
      properties:
        id:
          type: integer
          readOnly: true
          description: 自動生成された顧客ID
        name:
          type: string
          description: 顧客の名前
        email:
          type: string
          format: email
          description: 一意なメールアドレス
        phone:
          type: string
          description: 電話番号
      required:
        - id
        - name
        - email
    CustomerInput:
      type: object
      properties:
        name:
          type: string
          description: 顧客の名前
        email:
          type: string
          format: email
          description: 一意なメールアドレス
        phone:
          type: string
          description: 電話番号
      required:
        - name
        - email

Zod

sh
pnpx typed-openapi openapi.yaml -o apiClient.ts -r zod

apiClient.ts

ts
import { z } from 'zod'

export type Customer = z.infer<typeof Customer>
export const Customer = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string(),
  phone: z.union([z.string(), z.undefined()]).optional(),
})

export type CustomerInput = z.infer<typeof CustomerInput>
export const CustomerInput = z.object({
  name: z.string(),
  email: z.string(),
  phone: z.union([z.string(), z.undefined()]).optional(),
})

export type get_Customers = typeof get_Customers
export const get_Customers = {
  method: z.literal('GET'),
  path: z.literal('/customers'),
  requestFormat: z.literal('json'),
  parameters: z.never(),
  response: z.array(Customer),
}

export type post_Customers = typeof post_Customers
export const post_Customers = {
  method: z.literal('POST'),
  path: z.literal('/customers'),
  requestFormat: z.literal('json'),
  parameters: z.object({
    body: CustomerInput,
  }),
  response: Customer,
}

export type get_CustomersId = typeof get_CustomersId
export const get_CustomersId = {
  method: z.literal('GET'),
  path: z.literal('/customers/{id}'),
  requestFormat: z.literal('json'),
  parameters: z.object({
    path: z.object({
      id: z.number(),
    }),
  }),
  response: Customer,
}

export type put_CustomersId = typeof put_CustomersId
export const put_CustomersId = {
  method: z.literal('PUT'),
  path: z.literal('/customers/{id}'),
  requestFormat: z.literal('json'),
  parameters: z.object({
    path: z.object({
      id: z.number(),
    }),
    body: CustomerInput,
  }),
  response: Customer,
}

export type delete_CustomersId = typeof delete_CustomersId
export const delete_CustomersId = {
  method: z.literal('DELETE'),
  path: z.literal('/customers/{id}'),
  requestFormat: z.literal('json'),
  parameters: z.object({
    path: z.object({
      id: z.number(),
    }),
  }),
  response: z.unknown(),
}

// <EndpointByMethod>
export const EndpointByMethod = {
  get: {
    '/customers': get_Customers,
    '/customers/{id}': get_CustomersId,
  },
  post: {
    '/customers': post_Customers,
  },
  put: {
    '/customers/{id}': put_CustomersId,
  },
  delete: {
    '/customers/{id}': delete_CustomersId,
  },
}
export type EndpointByMethod = typeof EndpointByMethod
// </EndpointByMethod>

// <EndpointByMethod.Shorthands>
export type GetEndpoints = EndpointByMethod['get']
export type PostEndpoints = EndpointByMethod['post']
export type PutEndpoints = EndpointByMethod['put']
export type DeleteEndpoints = EndpointByMethod['delete']
export type AllEndpoints = EndpointByMethod[keyof EndpointByMethod]
// </EndpointByMethod.Shorthands>

// <ApiClientTypes>
export type EndpointParameters = {
  body?: unknown
  query?: Record<string, unknown>
  header?: Record<string, unknown>
  path?: Record<string, unknown>
}

export type MutationMethod = 'post' | 'put' | 'patch' | 'delete'
export type Method = 'get' | 'head' | 'options' | MutationMethod

type RequestFormat = 'json' | 'form-data' | 'form-url' | 'binary' | 'text'

export type DefaultEndpoint = {
  parameters?: EndpointParameters | undefined
  response: unknown
}

export type Endpoint<TConfig extends DefaultEndpoint = DefaultEndpoint> = {
  operationId: string
  method: Method
  path: string
  requestFormat: RequestFormat
  parameters?: TConfig['parameters']
  meta: {
    alias: string
    hasParameters: boolean
    areParametersRequired: boolean
  }
  response: TConfig['response']
}

type Fetcher = (
  method: Method,
  url: string,
  parameters?: EndpointParameters | undefined,
) => Promise<Endpoint['response']>

type RequiredKeys<T> = {
  [P in keyof T]-?: undefined extends T[P] ? never : P
}[keyof T]

type MaybeOptionalArg<T> = RequiredKeys<T> extends never ? [config?: T] : [config: T]

// </ApiClientTypes>

// <ApiClient>
export class ApiClient {
  baseUrl: string = ''

  constructor(public fetcher: Fetcher) {}

  setBaseUrl(baseUrl: string) {
    this.baseUrl = baseUrl
    return this
  }

  // <ApiClient.get>
  get<Path extends keyof GetEndpoints, TEndpoint extends GetEndpoints[Path]>(
    path: Path,
    ...params: MaybeOptionalArg<z.infer<TEndpoint['parameters']>>
  ): Promise<z.infer<TEndpoint['response']>> {
    return this.fetcher('get', this.baseUrl + path, params[0]) as Promise<z.infer<TEndpoint['response']>>
  }
  // </ApiClient.get>

  // <ApiClient.post>
  post<Path extends keyof PostEndpoints, TEndpoint extends PostEndpoints[Path]>(
    path: Path,
    ...params: MaybeOptionalArg<z.infer<TEndpoint['parameters']>>
  ): Promise<z.infer<TEndpoint['response']>> {
    return this.fetcher('post', this.baseUrl + path, params[0]) as Promise<z.infer<TEndpoint['response']>>
  }
  // </ApiClient.post>

  // <ApiClient.put>
  put<Path extends keyof PutEndpoints, TEndpoint extends PutEndpoints[Path]>(
    path: Path,
    ...params: MaybeOptionalArg<z.infer<TEndpoint['parameters']>>
  ): Promise<z.infer<TEndpoint['response']>> {
    return this.fetcher('put', this.baseUrl + path, params[0]) as Promise<z.infer<TEndpoint['response']>>
  }
  // </ApiClient.put>

  // <ApiClient.delete>
  delete<Path extends keyof DeleteEndpoints, TEndpoint extends DeleteEndpoints[Path]>(
    path: Path,
    ...params: MaybeOptionalArg<z.infer<TEndpoint['parameters']>>
  ): Promise<z.infer<TEndpoint['response']>> {
    return this.fetcher('delete', this.baseUrl + path, params[0]) as Promise<z.infer<TEndpoint['response']>>
  }
  // </ApiClient.delete>
}

export function createApiClient(fetcher: Fetcher, baseUrl?: string) {
  return new ApiClient(fetcher).setBaseUrl(baseUrl ?? '')
}

/**
 Example usage:
 const api = createApiClient((method, url, params) =>
   fetch(url, { method, body: JSON.stringify(params) }).then((res) => res.json()),
 );
 api.get("/users").then((users) => console.log(users));
 api.post("/users", { body: { name: "John" } }).then((user) => console.log(user));
 api.put("/users/:id", { path: { id: 1 }, body: { name: "John" } }).then((user) => console.log(user));
*/

// </ApiClient

Valibot

sh
pnpx typed-openapi openapi.yaml -o apiClient.ts -r valibot

apiClient.ts

ts
import * as v from 'valibot'

export type Customer = v.InferOutput<typeof Customer>
export const Customer = v.object({
  id: v.number(),
  name: v.string(),
  email: v.string(),
  phone: v.optional(v.union([v.string(), v.undefined_()])),
})

export type CustomerInput = v.InferOutput<typeof CustomerInput>
export const CustomerInput = v.object({
  name: v.string(),
  email: v.string(),
  phone: v.optional(v.union([v.string(), v.undefined_()])),
})

export type __ENDPOINTS_START__ = v.InferOutput<typeof __ENDPOINTS_START__>
export const __ENDPOINTS_START__ = v.object({})

export type get_Customers = v.InferOutput<typeof get_Customers>
export const get_Customers = v.object({
  method: v.literal('GET'),
  path: v.literal('/customers'),
  requestFormat: v.literal('json'),
  parameters: v.never(),
  response: v.array(Customer),
})

export type post_Customers = v.InferOutput<typeof post_Customers>
export const post_Customers = v.object({
  method: v.literal('POST'),
  path: v.literal('/customers'),
  requestFormat: v.literal('json'),
  parameters: v.object({
    body: CustomerInput,
  }),
  response: Customer,
})

export type get_CustomersId = v.InferOutput<typeof get_CustomersId>
export const get_CustomersId = v.object({
  method: v.literal('GET'),
  path: v.literal('/customers/{id}'),
  requestFormat: v.literal('json'),
  parameters: v.object({
    path: v.object({
      id: v.number(),
    }),
  }),
  response: Customer,
})

export type put_CustomersId = v.InferOutput<typeof put_CustomersId>
export const put_CustomersId = v.object({
  method: v.literal('PUT'),
  path: v.literal('/customers/{id}'),
  requestFormat: v.literal('json'),
  parameters: v.object({
    path: v.object({
      id: v.number(),
    }),
    body: CustomerInput,
  }),
  response: Customer,
})

export type delete_CustomersId = v.InferOutput<typeof delete_CustomersId>
export const delete_CustomersId = v.object({
  method: v.literal('DELETE'),
  path: v.literal('/customers/{id}'),
  requestFormat: v.literal('json'),
  parameters: v.object({
    path: v.object({
      id: v.number(),
    }),
  }),
  response: v.unknown(),
})

export type __ENDPOINTS_END__ = v.InferOutput<typeof __ENDPOINTS_END__>
export const __ENDPOINTS_END__ = v.object({})

// <EndpointByMethod>
export const EndpointByMethod = {
  get: {
    '/customers': get_Customers,
    '/customers/{id}': get_CustomersId,
  },
  post: {
    '/customers': post_Customers,
  },
  put: {
    '/customers/{id}': put_CustomersId,
  },
  delete: {
    '/customers/{id}': delete_CustomersId,
  },
}
export type EndpointByMethod = typeof EndpointByMethod
// </EndpointByMethod>

// <EndpointByMethod.Shorthands>
export type GetEndpoints = EndpointByMethod['get']
export type PostEndpoints = EndpointByMethod['post']
export type PutEndpoints = EndpointByMethod['put']
export type DeleteEndpoints = EndpointByMethod['delete']
export type AllEndpoints = EndpointByMethod[keyof EndpointByMethod]
// </EndpointByMethod.Shorthands>

// <ApiClientTypes>
export type EndpointParameters = {
  body?: unknown
  query?: Record<string, unknown>
  header?: Record<string, unknown>
  path?: Record<string, unknown>
}

export type MutationMethod = 'post' | 'put' | 'patch' | 'delete'
export type Method = 'get' | 'head' | 'options' | MutationMethod

type RequestFormat = 'json' | 'form-data' | 'form-url' | 'binary' | 'text'

export type DefaultEndpoint = {
  parameters?: EndpointParameters | undefined
  response: unknown
}

export type Endpoint<TConfig extends DefaultEndpoint = DefaultEndpoint> = {
  operationId: string
  method: Method
  path: string
  requestFormat: RequestFormat
  parameters?: TConfig['parameters']
  meta: {
    alias: string
    hasParameters: boolean
    areParametersRequired: boolean
  }
  response: TConfig['response']
}

type Fetcher = (
  method: Method,
  url: string,
  parameters?: EndpointParameters | undefined,
) => Promise<Endpoint['response']>

type RequiredKeys<T> = {
  [P in keyof T]-?: undefined extends T[P] ? never : P
}[keyof T]

type MaybeOptionalArg<T> = RequiredKeys<T> extends never ? [config?: T] : [config: T]

// </ApiClientTypes>

// <ApiClient>
export class ApiClient {
  baseUrl: string = ''

  constructor(public fetcher: Fetcher) {}

  setBaseUrl(baseUrl: string) {
    this.baseUrl = baseUrl
    return this
  }

  // <ApiClient.get>
  get<Path extends keyof GetEndpoints, TEndpoint extends GetEndpoints[Path]>(
    path: Path,
    ...params: MaybeOptionalArg<v.InferOutput<TEndpoint>['parameters']>
  ): Promise<v.InferOutput<TEndpoint>['response']> {
    return this.fetcher('get', this.baseUrl + path, params[0]) as Promise<v.InferOutput<TEndpoint>['response']>
  }
  // </ApiClient.get>

  // <ApiClient.post>
  post<Path extends keyof PostEndpoints, TEndpoint extends PostEndpoints[Path]>(
    path: Path,
    ...params: MaybeOptionalArg<v.InferOutput<TEndpoint>['parameters']>
  ): Promise<v.InferOutput<TEndpoint>['response']> {
    return this.fetcher('post', this.baseUrl + path, params[0]) as Promise<v.InferOutput<TEndpoint>['response']>
  }
  // </ApiClient.post>

  // <ApiClient.put>
  put<Path extends keyof PutEndpoints, TEndpoint extends PutEndpoints[Path]>(
    path: Path,
    ...params: MaybeOptionalArg<v.InferOutput<TEndpoint>['parameters']>
  ): Promise<v.InferOutput<TEndpoint>['response']> {
    return this.fetcher('put', this.baseUrl + path, params[0]) as Promise<v.InferOutput<TEndpoint>['response']>
  }
  // </ApiClient.put>

  // <ApiClient.delete>
  delete<Path extends keyof DeleteEndpoints, TEndpoint extends DeleteEndpoints[Path]>(
    path: Path,
    ...params: MaybeOptionalArg<v.InferOutput<TEndpoint>['parameters']>
  ): Promise<v.InferOutput<TEndpoint>['response']> {
    return this.fetcher('delete', this.baseUrl + path, params[0]) as Promise<v.InferOutput<TEndpoint>['response']>
  }
  // </ApiClient.delete>
}

export function createApiClient(fetcher: Fetcher, baseUrl?: string) {
  return new ApiClient(fetcher).setBaseUrl(baseUrl ?? '')
}

/**
 Example usage:
 const api = createApiClient((method, url, params) =>
   fetch(url, { method, body: JSON.stringify(params) }).then((res) => res.json()),
 );
 api.get("/users").then((users) => console.log(users));
 api.post("/users", { body: { name: "John" } }).then((user) => console.log(user));
 api.put("/users/:id", { path: { id: 1 }, body: { name: "John" } }).then((user) => console.log(user));
*/

// </ApiClient