60
28

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Aspidaに感動しちゃった件について

Last updated at Posted at 2021-02-18

はじめに

突然ですが。Aspidaって知ってますか??
僕は、知りませんでした。。
仕事先の先輩から教えてもらい感動しちゃったので、記事にしちゃいました。w

Aspidaとは?

aspida.png

  • 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

60
28
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
60
28

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?