はじめに
突然ですが。Aspidaって知ってますか??
僕は、知りませんでした。。
仕事先の先輩から教えてもらい感動しちゃったので、記事にしちゃいました。w
Aspidaとは?
- TypeScriptフレンドリーなHTTPクライアントの為のライブラリです。
- GitHub: Aspida
Aspidaの利点
- HTTPクライアントである[axios]、[ky]、[fetch]を使用したAPI リクエスト/レスポンスに型を付与できる点
- リクエストを文字列ではなく、プロパティ経由で行えるようになる点
試してみる
今回は、Swaggerをaspidaの型定義ファイルに一発変換してみたいと思います。
=> めっちゃ感動しました。。
また今回は、Swagger Petstoreを使用しました。
インストール
HTTPクライアントである[axios][ky][fetch]から選択する。
今回はaxiosを使っていきます。
npm init -y
npm install @aspida/axios axios
apiディレクトリを作成する
mkdir api
package.jsonに型定義ファイルをビルドする設定を入れる
package.json
{
"scripts": {
"api:build": "aspida"
}
}
aspidaの設定ファイルを作成する
aspida.config.js
module.exports = {
input: "api",
outputEachDir: true,
openapi: { inputFile: "https://petstore.swagger.io/v2/swagger.json" }
};
型定義の生成
npx openapi2aspida
実行結果
@types.ts
/* eslint-disable */
export type ApiResponse = {
code: number
type: string
message: string
}
export type Category = {
id: number
name: string
}
export type Pet = {
id?: number
category?: Category
name: string
photoUrls: string[]
tags?: Tag[]
status?: 'available' | 'pending' | 'sold'
}
export type Tag = {
id: number
name: string
}
export type Order = {
id: number
petId: number
quantity: number
shipDate: string
status: 'placed' | 'approved' | 'delivered'
complete: boolean
}
export type User = {
id: number
username: string
firstName: string
lastName: string
email: string
password: string
phone: string
userStatus: number
}
$api.ts
/* eslint-disable */
import { AspidaClient, BasicHeaders, dataToURLString } from 'aspida'
import { Methods as Methods0 } from './pet'
import { Methods as Methods1 } from './pet/_petId@number'
import { Methods as Methods2 } from './pet/_petId@number/uploadImage'
import { Methods as Methods3 } from './pet/findByStatus'
import { Methods as Methods4 } from './store/inventory'
import { Methods as Methods5 } from './store/order'
import { Methods as Methods6 } from './store/order/_orderId@number'
import { Methods as Methods7 } from './user'
import { Methods as Methods8 } from './user/_username@string'
import { Methods as Methods9 } from './user/createWithArray'
import { Methods as Methods10 } from './user/createWithList'
import { Methods as Methods11 } from './user/login'
const api = <T>({ baseURL, fetch }: AspidaClient<T>) => {
const prefix = (baseURL === undefined ? 'https://petstore.swagger.io/v2' : baseURL).replace(/\/$/, '')
const PATH0 = '/pet'
const PATH1 = '/uploadImage'
const PATH2 = '/pet/findByStatus'
const PATH3 = '/store/inventory'
const PATH4 = '/store/order'
const PATH5 = '/user'
const PATH6 = '/user/createWithArray'
const PATH7 = '/user/createWithList'
const PATH8 = '/user/login'
const GET = 'GET'
const POST = 'POST'
const PUT = 'PUT'
const DELETE = 'DELETE'
return {
pet: {
_petId: (val1: number) => {
const prefix1 = `${PATH0}/${val1}`
return {
uploadImage: {
post: (option: { body: Methods2['post']['reqBody'], config?: T }) =>
fetch<Methods2['post']['resBody'], BasicHeaders, Methods2['post']['status']>(prefix, `${prefix1}${PATH1}`, POST, option, 'FormData').json(),
$post: (option: { body: Methods2['post']['reqBody'], config?: T }) =>
fetch<Methods2['post']['resBody'], BasicHeaders, Methods2['post']['status']>(prefix, `${prefix1}${PATH1}`, POST, option, 'FormData').json().then(r => r.body),
$path: () => `${prefix}${prefix1}${PATH1}`
},
get: (option?: { config?: T }) =>
fetch<Methods1['get']['resBody'], BasicHeaders, Methods1['get']['status']>(prefix, prefix1, GET, option).json(),
$get: (option?: { config?: T }) =>
fetch<Methods1['get']['resBody'], BasicHeaders, Methods1['get']['status']>(prefix, prefix1, GET, option).json().then(r => r.body),
post: (option: { body: Methods1['post']['reqBody'], config?: T }) =>
fetch(prefix, prefix1, POST, option, 'URLSearchParams').send(),
$post: (option: { body: Methods1['post']['reqBody'], config?: T }) =>
fetch(prefix, prefix1, POST, option, 'URLSearchParams').send().then(r => r.body),
delete: (option?: { headers?: Methods1['delete']['reqHeaders'], config?: T }) =>
fetch(prefix, prefix1, DELETE, option).send(),
$delete: (option?: { headers?: Methods1['delete']['reqHeaders'], config?: T }) =>
fetch(prefix, prefix1, DELETE, option).send().then(r => r.body),
$path: () => `${prefix}${prefix1}`
}
},
findByStatus: {
get: (option: { query: Methods3['get']['query'], config?: T }) =>
fetch<Methods3['get']['resBody'], BasicHeaders, Methods3['get']['status']>(prefix, PATH2, GET, option).json(),
$get: (option: { query: Methods3['get']['query'], config?: T }) =>
fetch<Methods3['get']['resBody'], BasicHeaders, Methods3['get']['status']>(prefix, PATH2, GET, option).json().then(r => r.body),
$path: (option?: { method?: 'get'; query: Methods3['get']['query'] }) =>
`${prefix}${PATH2}${option && option.query ? `?${dataToURLString(option.query)}` : ''}`
},
post: (option: { body: Methods0['post']['reqBody'], config?: T }) =>
fetch(prefix, PATH0, POST, option).send(),
$post: (option: { body: Methods0['post']['reqBody'], config?: T }) =>
fetch(prefix, PATH0, POST, option).send().then(r => r.body),
put: (option: { body: Methods0['put']['reqBody'], config?: T }) =>
fetch(prefix, PATH0, PUT, option).send(),
$put: (option: { body: Methods0['put']['reqBody'], config?: T }) =>
fetch(prefix, PATH0, PUT, option).send().then(r => r.body),
$path: () => `${prefix}${PATH0}`
},
store: {
inventory: {
get: (option?: { config?: T }) =>
fetch<Methods4['get']['resBody'], BasicHeaders, Methods4['get']['status']>(prefix, PATH3, GET, option).json(),
$get: (option?: { config?: T }) =>
fetch<Methods4['get']['resBody'], BasicHeaders, Methods4['get']['status']>(prefix, PATH3, GET, option).json().then(r => r.body),
$path: () => `${prefix}${PATH3}`
},
order: {
_orderId: (val2: number) => {
const prefix2 = `${PATH4}/${val2}`
return {
get: (option?: { config?: T }) =>
fetch<Methods6['get']['resBody'], BasicHeaders, Methods6['get']['status']>(prefix, prefix2, GET, option).json(),
$get: (option?: { config?: T }) =>
fetch<Methods6['get']['resBody'], BasicHeaders, Methods6['get']['status']>(prefix, prefix2, GET, option).json().then(r => r.body),
delete: (option?: { config?: T }) =>
fetch(prefix, prefix2, DELETE, option).send(),
$delete: (option?: { config?: T }) =>
fetch(prefix, prefix2, DELETE, option).send().then(r => r.body),
$path: () => `${prefix}${prefix2}`
}
},
post: (option: { body: Methods5['post']['reqBody'], config?: T }) =>
fetch<Methods5['post']['resBody'], BasicHeaders, Methods5['post']['status']>(prefix, PATH4, POST, option).json(),
$post: (option: { body: Methods5['post']['reqBody'], config?: T }) =>
fetch<Methods5['post']['resBody'], BasicHeaders, Methods5['post']['status']>(prefix, PATH4, POST, option).json().then(r => r.body),
$path: () => `${prefix}${PATH4}`
}
},
user: {
_username: (val1: string) => {
const prefix1 = `${PATH5}/${val1}`
return {
get: (option?: { config?: T }) =>
fetch<Methods8['get']['resBody'], BasicHeaders, Methods8['get']['status']>(prefix, prefix1, GET, option).json(),
$get: (option?: { config?: T }) =>
fetch<Methods8['get']['resBody'], BasicHeaders, Methods8['get']['status']>(prefix, prefix1, GET, option).json().then(r => r.body),
put: (option: { body: Methods8['put']['reqBody'], config?: T }) =>
fetch(prefix, prefix1, PUT, option).send(),
$put: (option: { body: Methods8['put']['reqBody'], config?: T }) =>
fetch(prefix, prefix1, PUT, option).send().then(r => r.body),
delete: (option?: { config?: T }) =>
fetch(prefix, prefix1, DELETE, option).send(),
$delete: (option?: { config?: T }) =>
fetch(prefix, prefix1, DELETE, option).send().then(r => r.body),
$path: () => `${prefix}${prefix1}`
}
},
createWithArray: {
post: (option: { body: Methods9['post']['reqBody'], config?: T }) =>
fetch(prefix, PATH6, POST, option).send(),
$post: (option: { body: Methods9['post']['reqBody'], config?: T }) =>
fetch(prefix, PATH6, POST, option).send().then(r => r.body),
$path: () => `${prefix}${PATH6}`
},
createWithList: {
post: (option: { body: Methods10['post']['reqBody'], config?: T }) =>
fetch(prefix, PATH7, POST, option).send(),
$post: (option: { body: Methods10['post']['reqBody'], config?: T }) =>
fetch(prefix, PATH7, POST, option).send().then(r => r.body),
$path: () => `${prefix}${PATH7}`
},
login: {
get: (option: { query: Methods11['get']['query'], config?: T }) =>
fetch<Methods11['get']['resBody'], Methods11['get']['resHeaders'], Methods11['get']['status']>(prefix, PATH8, GET, option).text(),
$get: (option: { query: Methods11['get']['query'], config?: T }) =>
fetch<Methods11['get']['resBody'], Methods11['get']['resHeaders'], Methods11['get']['status']>(prefix, PATH8, GET, option).text().then(r => r.body),
$path: (option?: { method?: 'get'; query: Methods11['get']['query'] }) =>
`${prefix}${PATH8}${option && option.query ? `?${dataToURLString(option.query)}` : ''}`
},
post: (option: { body: Methods7['post']['reqBody'], config?: T }) =>
fetch(prefix, PATH5, POST, option).send(),
$post: (option: { body: Methods7['post']['reqBody'], config?: T }) =>
fetch(prefix, PATH5, POST, option).send().then(r => r.body),
$path: () => `${prefix}${PATH5}`
}
}
}
export type ApiInstance = ReturnType<typeof api>
export default api
おわりに
いやまじ、凄すぎです笑
参考
HTTPリクエストを型安全にする手法とOSS
SwaggerをTypeScriptに変換してAPIリクエストで型チェックを有効にする
https://github.com/aspida/openapi2aspida
https://github.com/aspida/aspida/tree/master/packages/aspida/docs/ja#readme