💨

Stoplight StudioとOpenAPI Generatorを使ってみる

2022/06/15に公開

Nuxt(SPA)とLaravelのプロジェクトで、Stoplight StudioとOpenAPI Generatorを導入してみました。スキーマを定義してフロントエンドの型を生成するまでのメモを書きたいと思います。

検証用のリポジトリはこちらです。

https://github.com/tekihei2317/stoplight-and-typescript-frontend

環境

  • @openapitools/openapi-generator-cli@2.5.1

Stoplight Studioとは

Stoplight Studioは、OpenAPIの定義ファイルをGUIで作成できるツールです。定義したエンドポイントにPostmanのようにリクエストを送ったり、組み込みのモックサーバーを使ってダミーのAPIを作ることができます。

APIの定義

ディレクトリを作成し、作成したディレクトリをStoplight Studioで開きます。モノレポにしているため、プロジェクトルートにapispecというディレクトリを作りました。

1点注意が必要なのは、openapi-generatorがOAS3.1にまだ対応していないことです。そのため、Stoplight Studioでバージョンを選択するときは、3.0を選択します。

https://i.gyazo.com/7d559cefa0c3e737da271ee622d35b4a.png

openapi-generatorが使用しているswagger-parserが最近OAS3.1に対応したみたいなので、これから3.1対応が進みそうな気配があります。

https://github.com/OpenAPITools/openapi-generator/issues/9083

Stoplight Studioでは、エンドポイントとリクエスト、レスポンスを定義します。このあたりはGUIをポチポチするだけでした。今回はタスクの詳細と編集のAPIスキーマを定義してみました。

スキーマ定義
apispec/references/todo-app.yml
openapi: 3.0.0
x-stoplight:
  id: dmiy7jxs5yhc4
info:
  title: todo-app
  version: "1.0"
servers:
  - url: "http://localhost:3100"
paths:
  "/tasks/{taskId}":
    parameters:
      - schema:
          type: string
        name: taskId
        in: path
        required: true
    get:
      summary: タスク詳細を取得する
      tags:
        - tasks
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: integer
                  name:
                    type: string
                  description:
                    type: string
                    nullable: true
                  status:
                    type: string
                  status_id:
                    type: integer
                    minimum: 1
                    maximum: 5
                required:
                  - id
                  - name
                  - description
                  - status
                  - status_id
      operationId: get-tasks-taskId
    put:
      summary: タスクを更新する
      tags:
        - tasks
      responses:
        "204":
          description: No Content
      operationId: put-tasks-taskId
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                description:
                  type: string
                status_id:
                  type: number
              required:
                - name
                - status_id
components:
  schemas: {}

型の自動生成

openapi-generatorをインストールします。yarnでインストールしました。

yarn add -D @openapitools/openapi-generator-cli

以下のコマンドで、APIクライアントと型定義を生成します。

openapi-generator-cli generate -i ../apispec/reference/todo-app.yaml -g typescript-fetch -o api -c ./openapi-generator.json

必須パラメータはAPI定義ファイルのパス(-i)と、ジェネレーターのテンプレート(-g)です。型だけを使用したかったので、テンプレートはシンプルそうなtypescript-fetchを選択しました。その他、出力ディレクトリと設定ファイルを指定しています。

パラメータ名 説明
-i API定義ファイルのパス
-g ジェネレーターのテンプレート
-o 出力ディレクトリ
-c 設定ファイルのパス

設定ファイルにはプロパティ名をスネークケースにする設定を書きました。

openapi-generator.json
{
  "modelPropertyNaming": "snake_case"
}

コマンドを実行すると、以下のようなファイルが作られます。

api
├── apis
│   ├── TasksApi.ts
│   └── index.ts
├── index.ts
├── models
│   ├── GetTasksTaskId200Response.ts
│   ├── PutTasksTaskIdRequest.ts
│   └── index.ts
└── runtime.ts

apis/の中身がAPIクライアントです(今回は使用しません)。models/の中に定義したリクエストやレスポンスの型が含まれています。modelには以下のような型定義が書かれています。

// タスク詳細APIのレスポンスボディの型
export interface GetTasksTaskId200Response {
  id: number;
  name: string;
  description: string | null;
  status: string;
  status_id: number;
}

// タスク編集APIのリクエストボディの型
export interface PutTasksTaskIdRequest {
    name: string;
    description?: string;
    status_id: number;
}

リクエストからフォームの型に変換する

生成された型を、フォームに合わせた型に変換する型をつくります。フォームの初期値はnullにしたかったので、プロパティにnull型を加えています。

// TODO: object以外のいい感じの型を使う
export type RequestToForm<T extends object> = {
  [K in keyof T]-?: T[K] extends object ? RequestToForm<T[K]> : T[K] | null
}

-?で省略可能を外しているのは、(作っていたアプリが業務システムなのもあり)省略可能なプロパティがたくさんあったため、フロントエンドで代入し忘れないようにするためです。

form.value = dataFromApi // refにまるっと代入するとき、フィールドが省略可能だと渡し忘れに気づけない

コンポーネントでは以下のように使います。

// テンプレート部分は省略
import axios from "axios";
import { reactive } from "vue";
import { GetTasksTaskId200Response, PutTasksTaskIdRequest } from "./api";
import { RequestToForm } from "./utils/form";

const taskId = 1;
const endpoint = `http://localhost:3100/tasks/${taskId}`;

// リクエストボディの型から、フォームの型を作成する
type EditTaskForm = RequestToForm<PutTasksTaskIdRequest>;

// フォームを初期化する
const editForm = reactive<EditTaskForm>({
  name: null,
  description: null,
  status_id: null,
});

// APIからデータを取得する
axios.get<GetTasksTaskId200Response>(endpoint).then((response) => {
  editForm.name = response.data.name;
  editForm.description = response.data.description;
  editForm.status_id = response.data.status_id;
});

// フォームを送信する
const onSubmit = () => {
  axios.put(endpoint, editForm).then(() => {
    alert("タスクを更新しました");
  });
};

// テンプレート部分は省略

まとめ

  • OpenAPI GeneratorがOAS3.1に未対応のため、Stoplight Studioでは3.0を使用する
  • openapi-generatorを使うと、定義ファイルからTypeScriptの型定義を自動生成できる
  • 自動生成したAPIの型をフォーム用に変換する型を書けばいいかもしれない

モックサーバーを使用したテストや、API定義とAPIを一致させるための仕組みはまだできていないので、今後実践していければと思います。GraphQLがうらやましい

参考

GitHubで編集を提案

Discussion