🐮
aspidaでOpenAPIからクライアント用の型とコードを自動生成する
OpenAPIのスキーマからクライアント用の型とコードを自動生成する方法について、以下のサイトのまとめが参考になりました。
aspida
その中から、 aspida というライブラリが良さそうだったので試してみました。
aspidaは日本人が作成した、TypeScriptに特化したツールで、作者の方の記事を参考にさせていただきました。
記事の趣旨
openapi2aspida + @aspida/axios を使って、簡単に試してみます。
後述しますが、2023/03/08時点で一番大事なのは、axiosのバージョンをv0系を使うことです。
手順
OpanAPIスキーマを準備する
サンプルとして、Bookの取得と作成のAPIを用意しました。
Bookの定義
openapi.yml
openapi: 3.0.3
info:
title: Sample API
version: "1.0"
servers:
- url: "http://localhost:3000"
paths:
/books/{id}:
parameters:
- schema:
type: string
name: id
in: path
required: true
get:
operationId: get-book
responses:
"200":
description: Book
content:
application/json:
schema:
$ref: "#/components/schemas/Book"
examples:
Example 1:
value:
id: 1
status: published
name: Sample Book1
created_at: "2019-08-24T14:15:22Z"
"404":
description: User Not Found
/books:
post:
operationId: post-book
responses:
"200":
description: Book Created
content:
application/json:
schema:
$ref: "#/components/schemas/Book"
examples:
Example 1:
value:
id: 1
status: published
name: Sample Book1
created_at: "2019-08-24T14:15:22Z"
"400":
description: Missing Required Information
requestBody:
content:
application/json:
schema:
type: object
additionalProperties: false
properties:
name:
type: string
status:
type: string
enum:
- published
- draft
required:
- name
- status
examples:
Example 1:
value:
name: Sample Book1
status: published
components:
schemas:
Book:
type: object
additionalProperties: false
properties:
id:
type: integer
name:
type: string
status:
type: string
enum:
- published
- draft
created_at:
type: string
format: date-time
required:
- id
- name
- status
- created_at
型、コードを自動生成する
プロジェクトルートに上記の openapi.yml
を置いて、以下を実行します。
(openapi2aspidaはローカルにインストールしなくても良い)
npx openapi2aspida -i=openapi.yaml
作成されるファイル
これで api
配下に以下のようなフォルダが作成されます。
api
├── $api.ts
├── @types
│ └── index.ts
└── books
├── $api.ts
├── _id@string
│ └── index.ts
└── index.ts
@types/index.ts
Bookの型。statusのenumも正しく定義されています。
/* eslint-disable */
export type Book = {
id: number
name: string
status: 'published' | 'draft'
created_at: string
}
books/index.ts
Book作成APIの型定義
/* eslint-disable */
import type * as Types from '../@types'
export type Methods = {
post: {
status: 200
/** Book Created */
resBody: Types.Book
reqBody: {
name: string
status: 'published' | 'draft'
}
}
}
books/_id@string/index.ts
Book取得APIの型定義
/* eslint-disable */
import type * as Types from '../../@types'
export type Methods = {
get: {
status: 200
/** Book */
resBody: Types.Book
}
}
$api.ts, books/$api.ts
apiリクエスト用のコード
開く
$api.ts
import type { AspidaClient, BasicHeaders } from 'aspida'
import type { Methods as Methods0 } from './books'
import type { Methods as Methods1 } from './books/_id@string'
const api = <T>({ baseURL, fetch }: AspidaClient<T>) => {
const prefix = (baseURL === undefined ? 'http://localhost:3000' : baseURL).replace(/\/$/, '')
const PATH0 = '/books'
const GET = 'GET'
const POST = 'POST'
return {
books: {
_id: (val1: string) => {
const prefix1 = `${PATH0}/${val1}`
return {
/**
* @returns Book
*/
get: (option?: { config?: T | undefined } | undefined) =>
fetch<Methods1['get']['resBody'], BasicHeaders, Methods1['get']['status']>(prefix, prefix1, GET, option).json(),
/**
* @returns Book
*/
$get: (option?: { config?: T | undefined } | undefined) =>
fetch<Methods1['get']['resBody'], BasicHeaders, Methods1['get']['status']>(prefix, prefix1, GET, option).json().then(r => r.body),
$path: () => `${prefix}${prefix1}`
}
},
/**
* @returns Book Created
*/
post: (option: { body: Methods0['post']['reqBody'], config?: T | undefined }) =>
fetch<Methods0['post']['resBody'], BasicHeaders, Methods0['post']['status']>(prefix, PATH0, POST, option).json(),
/**
* @returns Book Created
*/
$post: (option: { body: Methods0['post']['reqBody'], config?: T | undefined }) =>
fetch<Methods0['post']['resBody'], BasicHeaders, Methods0['post']['status']>(prefix, PATH0, POST, option).json().then(r => r.body),
$path: () => `${prefix}${PATH0}`
}
}
}
export type ApiInstance = ReturnType<typeof api>
export default api
books/$api.ts
import type { AspidaClient, BasicHeaders } from 'aspida'
import type { Methods as Methods0 } from '.'
import type { Methods as Methods1 } from './_id@string'
const api = <T>({ baseURL, fetch }: AspidaClient<T>) => {
const prefix = (baseURL === undefined ? 'http://localhost:3000' : baseURL).replace(/\/$/, '')
const PATH0 = '/books'
const GET = 'GET'
const POST = 'POST'
return {
_id: (val0: string) => {
const prefix0 = `${PATH0}/${val0}`
return {
/**
* @returns Book
*/
get: (option?: { config?: T | undefined } | undefined) =>
fetch<Methods1['get']['resBody'], BasicHeaders, Methods1['get']['status']>(prefix, prefix0, GET, option).json(),
/**
* @returns Book
*/
$get: (option?: { config?: T | undefined } | undefined) =>
fetch<Methods1['get']['resBody'], BasicHeaders, Methods1['get']['status']>(prefix, prefix0, GET, option).json().then(r => r.body),
$path: () => `${prefix}${prefix0}`
}
},
/**
* @returns Book Created
*/
post: (option: { body: Methods0['post']['reqBody'], config?: T | undefined }) =>
fetch<Methods0['post']['resBody'], BasicHeaders, Methods0['post']['status']>(prefix, PATH0, POST, option).json(),
/**
* @returns Book Created
*/
$post: (option: { body: Methods0['post']['reqBody'], config?: T | undefined }) =>
fetch<Methods0['post']['resBody'], BasicHeaders, Methods0['post']['status']>(prefix, PATH0, POST, option).json().then(r => r.body),
$path: () => `${prefix}${PATH0}`
}
}
export type ApiInstance = ReturnType<typeof api>
export default api
リクエストする
パッケージ追加
ReactやVueなどのプロジェクトがある前提で、以下のパッケージを追加します。
npm i @aspida/axios axios@^0.27.2
リクエストする
import axiosClient from '@aspida/axios'
import axios from 'axios';
import api from "./api/$api"
// APIクライアントの設定
const axiosConfig = { baseURL: "http://localhost:3000" };
const client = api(aspida(axios, axiosConfig));
// id1のBookを取得する
client.books._id("1").$get();
// => { id: 1, status: 'published', name: 'Sample Book1', created_at: '2023-03-08T06:05:38' }
// Bookを作成する
client.books.$post({ body: { name: "Sample", status: "published" })
// => { id: 1, status: 'published', name: 'Sample', created_at: '2023-03-08T06:05:38' }
感想
API修正時に、OpenAPIとサーバー側を修正するだけでクライアント側のコードが自動修正されるのはとても便利そうです。
Discussion