💨

OpenAPI Generator TypeScript Axiosで型付きリクエストの自動生成 | Offers Tech Blog

2022/06/20に公開

概要

こんにちは。Offers を運営している株式会社 overflow の磯崎です。

今回は、OpenAPI Generator を使用して OpenAPI 定義ファイルを元にリクエストとその型を自動生成してフロントで使っていくと安心・安全・快適に開発できますというご紹介です。

予め定義した API 定義から以下を自動生成できます

  • request
  • body, reponse
  • model

スキーマ駆動開発を行っていれば、API 定義をまず最初に行い、それに合わせてバックエンドは開発を進め、フロントは API 定義を元に立ち上がったモックサーバーと共に開発していく流れになります。
その際、API 定義を絶対神としそこから自動で生成されたものを使うだけで、API 定義に沿った通信周りの処理を簡単且つ安全に組み込んでいくことができます。

自分でいじることなく自動生成されたリクエストの処理を使うだけなので、API に変更があった際も変更に追従しやすく、フロントとバックエンドでの API における乖離の発生リスクも減らすことができます。

Install

様々なコードの自動生成に対応 しているのですが、今回は typescript-axios を使用して、axios を使った通信部分を自動生成していきます。

まず、openapi-generator-cli をインストールします。

npm install @openapitools/openapi-generator-cli -D

リクエストの自動生成

まずAPIを定義

「ユーザー取得」、「ユーザー更新」の2つをここでは定義します。

openapi: 3.0.0
info:
  title: sample_api
  version: '1.0'
servers:
  - url: 'http://localhost:3000'
paths:
  '/users/{user_id}':
    parameters:
      - schema:
          type: string
        name: user_id
        in: path
        required: true
    get:
      summary: ユーザー取得
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  user:
                    type: object
                    properties:
                      name:
                        type: string
      operationId: get-user_id
      description: ''
    post:
      summary: ユーザー更新
      operationId: post-users-user_id
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  user:
                    type: object
                    properties:
                      name:
                        type: string
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                user:
                  type: object
                  properties:
                    name:
                      type: string
                    email:
                      type: string
          application/xml:
            schema:
              type: object
              properties:
                user:
                  type: object
                  properties:
                    name:
                      type: string
        description: ''
components:
  schemas: {}

自動生成を実行

上記実行ファイルを元に、生成していきます。

下記を実行していきます。

openapi-generator-cli generate -g <何を自動生成するのか> -i <API定義ymlのpath> -o <エクスポート先のディレクトリのpath>

例として、以下のように記述します。

openapi-generator-cli generate -g typescript-axios -i ./api_schema/reference/sample_api.v1.yaml -o ./types/typescript-axios

そしてこれを package.json に定義します。

"scripts": {
  "generate-typescript-axios": "openapi-generator-cli generate -g typescript-axios -i ./api_schema/reference/sample_api.v1.yaml -o ./types/typescript-axios"
},

そして実行
npm run generate-typescript-axios

生成されたファイルを確認

上記実行後、./types/typescript-axios 配下に色々とファイルが作成されます。
色々ファイルがありますが、リクエストや型は api.ts に出力されています。

api.ts に生成されたものは自動生成されたものなので、手を加えることはなく、適宜 import してこのまま使用していきます。

先程の API 定義を元に、下記の型やリクエストが生成されています。

Request Body

/**
 *
 * @export
 * @interface PostUsersUserIdRequest
 */
export interface PostUsersUserIdRequest {
    /**
     *
     * @type {PostUsersUserIdRequestUser}
     * @memberof PostUsersUserIdRequest
     */
    'user'?: PostUsersUserIdRequestUser;
}
/**
 *
 * @export
 * @interface PostUsersUserIdRequestUser
 */
export interface PostUsersUserIdRequestUser {
    /**
     *
     * @type {string}
     * @memberof PostUsersUserIdRequestUser
     */
    'name'?: string;
    /**
     *
     * @type {string}
     * @memberof PostUsersUserIdRequestUser
     */
    'email'?: string;
}

Response

/**
 *
 * @export
 * @interface GetUserId200Response
 */
export interface GetUserId200Response {
    /**
     *
     * @type {GetUserId200ResponseUser}
     * @memberof GetUserId200Response
     */
    'user'?: GetUserId200ResponseUser;
}

Request

export class DefaultApi extends BaseAPI {
    /**
     *
     * @summary ユーザー取得
     * @param {string} userId
     * @param {*} [options] Override http request option.
     * @throws {RequiredError}
     * @memberof DefaultApi
     */
    public getUserId(userId: string, options?: AxiosRequestConfig) {
        return DefaultApiFp(this.configuration).getUserId(userId, options).then((request) => request(this.axios, this.basePath));
    }

    /**
     *
     * @summary ユーザー更新
     * @param {string} userId
     * @param {PostUsersUserIdRequest} [postUsersUserIdRequest]
     * @param {*} [options] Override http request option.
     * @throws {RequiredError}
     * @memberof DefaultApi
     */
    public postUsersUserId(userId: string, postUsersUserIdRequest?: PostUsersUserIdRequest, options?: AxiosRequestConfig) {
        return DefaultApiFp(this.configuration).postUsersUserId(userId, postUsersUserIdRequest, options).then((request) => request(this.axios, this.basePath));
    }
}

初回実行後に設定ファイル openapitools.json が生成されます

初回、openapi-generator-cli generate ... 実行後に openapitools.json が生成されます。
自動生成に関する設定はこちらのファイルで管理していきます。

{
  "$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
  "spaces": 2,
  "generator-cli": {
    "version": "6.0.0"
  }
}

API 定義時の注意点

上記で生成したコードにいくつか違和感を覚える箇所がありますね。

  • DefaultApi というクラスが生成されている
    理想は User, Company などでカテゴライズしたい

  • postUsersUserId というメソッドそのまま呼び出す?名前が不格好
    理想は updateUser など普段している命名通り、行儀よくリーダブルにしたい

API 定義時の注意点その1 tag は必須

これが前述した「DefaultApi というクラスが生成されている」問題を解決します。
API 定義にしっかりと tag をつけることで、自動生成されるコードのクラスも分類され、整理されます。
ここでは、User に関連する API なので、user というタグを付けます。

get:
  summary: ユーザー取得
  responses:
    '200':
      description: OK
      content:
        application/json:
          schema:
            type: object
            properties:
              user:
                type: object
                properties:
                  name:
                    type: string
  operationId: get-user_id
  description: ''
  tags:
    - user

tags に user を設定しました。

もういっちょ自動生成してみると、このように UserAPI というクラスが生成され、User カテゴリの API だよというのがわかりやすくなります。

export class UserApi extends BaseAPI {
    /**
     *
     * @summary ユーザー取得
     * @param {string} userId
     * @param {*} [options] Override http request option.
     * @throws {RequiredError}
     * @memberof UserApi
     */
    public getUserId(userId: string, options?: AxiosRequestConfig) {
        return UserApiFp(this.configuration).getUserId(userId, options).then((request) => request(this.axios, this.basePath));
    }
}

API 定義時の注意点その2 OperationId も必須且つ行儀よく命名

OperationId をつけることで、上述した「postUsersUserId というメソッドそのまま呼び出す?名前が不格好」問題を解決できます。

OperationId はそのまま生成されるメソッド名となります。そしてそのメソッドを呼び出して通信処理を書いていくので、ここでの命名を怠るとよくわからない名前のメソッドが爆誕します。

普段通りしっかりと命名します。ここでは、「ユーザー更新」API なので、「update-user」と名付けます。

post:
  summary: ユーザー更新
  operationId: update-user

もう一度自動生成すると、しっかりと命名した名前でメソッドが自動生成されていることがわかります。

export class UserApi extends BaseAPI {
    /**
     *
     * @summary ユーザー更新
     * @param {string} userId
     * @param {UpdateUserRequest} [updateUserRequest]
     * @param {*} [options] Override http request option.
     * @throws {RequiredError}
     * @memberof UserApi
     */
    public updateUser(userId: string, updateUserRequest?: UpdateUserRequest, options?: AxiosRequestConfig) {
        return UserApiFp(this.configuration).updateUser(userId, updateUserRequest, options).then((request) => request(this.axios, this.basePath));
    }
}

これで直感的な名前になりましたね。

環境毎に API の BasePath を変更したい

これで API 定義を元に通信部分の自動生成ができました。後はこれをフロント側で呼び出すだけで、毎回自分で path を記述することなくリクエストを行えます。

しかし、実際の開発をしていくと環境に応じて API URL は変わるというのが想定されます。
例えば開発環境であればモックサーバーを使用し、本番であれば本番の API URL へと向き先を変更する必要があります。

その場合は、以下のように config をこちらで作成し、basePath を指定した上で各 API クラスに引数を渡してあげることで実現できます。

import { Configuration } from '@/types/typescript-axios/configuration';

const { API_URL } = process.env;

const config = new Configuration({
  basePath: API_URL,
});

const userApi = new UserApi(config);

任意に設定したaxiosを渡したい

このまま自動生成されたメソッドを使ってリクエストしていくのも良いですが、こちらで設定を変更した axios インスタンスを渡したいケースもよくあることかと思います。

その場合も第三引数に渡してあげることで、任意に定義した axios を渡すことができます。

import {
  UserApi,
} from '@/types/typescript-axios/api';

import axios from 'axios';

const axiosInstance = axios.create({
  baseURL: API_URL,
  withCredentials: true,
});

const userApi = new UserApi(config, '', axiosInstance);

自動生成されたコードはこのように、BaseAPI というクラスが継承されています。

export class UserApi extends BaseAPI {}

その BaseAPI は以下のようになっているため、任意に色々と渡すことができます。

export class BaseAPI {
  protected configuration: Configuration | undefined;

  constructor(configuration?: Configuration, protected basePath: string = BASE_PATH, protected axios: AxiosInstance = globalAxios) {
      if (configuration) {
          this.configuration = configuration;
          this.basePath = configuration.basePath || this.basePath;
      }
  }
};

実際に使用してみる

config や axios を渡した状態のインスタンスをまず定義し、exportしておく

import {
  UserApi,
} from '@/types/typescript-axios/api';

import { Configuration } from '@/types/typescript-axios/configuration';

import axios from 'axios';

const { API_URL } = process.env;

const config = new Configuration({
  basePath: API_URL,
});

const axiosInstance = axios.create({
  baseURL: API_URL,
  withCredentials: true,
});

const userApi = new UserApi(config, '', axiosInstance);

export {
  userApi
};

上記を呼び出すだけ

<script lang="ts">
import Vue from 'vue'
import { userApi } from '../api/';

export default Vue.extend({
  name: 'SamplePage',
  methods: {
    async getUserRequest() {
      const userId = 'xxx'
      const response = await userApi.getUserId(userId)
    }
  }
})
</script>

まとめ

API 定義を神としそれに追従する形でフロントを開発していく中で、この自動生成を取り入れるとスーパー楽ちん and 安全に開発をしていくことができます。
あとはバックエンド側で API 定義と実装にずれがないことをテストで保証できていれば、フロント・バックエンドでの食い違い発生リスクを抑えて開発を進めていくことができます。

今回のサンプルコード

https://github.com/keitaisozaki/sample_open_api_generator

関連記事

https://zenn.dev/offers/articles/20220411-open-api-schema
https://zenn.dev/offers/articles/20220530-openapi_stoplight

Offers Tech Blog

Discussion