29
19

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 3 years have passed since last update.

OpenAPI3 + OpenAPI generator でgolangサーバ・TypeScriptクライアントの実装を試す

Posted at

昨今のマイクロサービス化の流れに伴い、サービス間の情報のやり取りのルールをスキーマとして定義してクライアント、サーバーの開発を行うスキーマ駆動開発が取りた出されている。

今回はOpenAPI3(旧Swagger)をスキーマ駆動開発のツールの一つとして取り上げたが、意外と踏み込んだHello Worldの記事が少なかったので書いた。

対象読者

  • 最近swaggerやOpenAPIという言葉を聞いて試してみたいが導入に悩んでいる人
  • 普段からAPIを使用した開発を行っているがOpenAPIを使ったことがない人
  • Dockerや特定プログラミングをある程度まで習熟している人

OpenAPIとは

REST APIのリクエスト方法やパラメータなどの仕様の記述する定義。
OpenAPIはswaggerで使われていたツールが利用可能で、それらを利用することでAPI仕様書の表示・作成、API仕様に則ったサーバ・クライアントのコード生成が行えるようになる。

OpenAPIのツールでもっとも重要なものは コードジェネレータ だと自分は認識している。
コードジェネレータがなければ、究極的にはスキーマはただのドキュメントと何も変わらない。
コードジェネレータを使うことでスキーマの変更に追従を自動化することが可能になり、CI/CDでズレの検知が可能になるので放置されることがないためだ。

メリット

  • API利用者側のクライアントコードの実装が容易になる
  • スキーマを定義することで仕様書を簡単に生成することが可能
  • スキーマ(インターフェイスと言っても良い)が定まっているのでサーバ・クライアントで独立して開発が可能
    • クライアント側はモックサーバやダミーデータを用意しやすい
  • コードジェネレータを用いることでAPIドキュメントと実際のコードが一致させ続けることが用意

デメリット

  • ツールのサポートがあるとはいえ、スキーマの作成がそれなりに億劫
  • 最上流にスキーマが存在するので、スキーマを作成しないと開発が始められない
  • (当たり前だが)独自で仕様書を作成するのに比べて覚えることが多い

開発環境

https://github.com/d0riven/HelloApiSchema にコードは置いてある。

  • 計算機
    • Macbook
  • ツール
    • DockerDesktop 2.2.03 (docker 19.03.5)
    • OpenAPI 3.0.2
    • golang 1.13.8
    • TypeScript 3.8.2 (その他のツールは package.json を参考)

試す

以下の手順で試していく

  1. OpenAPI3でスキーマを作成
  2. モックサーバの構築
  • openapi-generatorでgolangのAPIサーバのコードを生成
  • ダミーデータを返す実装を追加
  1. クライアントの構築
  • openapi-generatorでTypeScriptのAPIクライアントのコードを生成
  • 2で作成したモックサーバを叩く

OpenAPI3でスキーマを作成

OpenAPI3の記法

作成する前にそもそもの書き方や構造を説明する.
ちなみにこの記事が古くなっている可能性はあるので、 Swagger Basic Structureも参考にすると良い。
また githubで仕様も公開されているのでこっちも参考にすると良い。

すべてを紹介はしきれないので重要な部分を紹介する。
スキーマ定義は https://qiita.com/teinen_qiita/items/e440ca7b1b52ec918f1b のほうがまとまっているので、こちらを参照しても良いかもしれない。

  • openapi
    • openapiのバージョン番号を記載(記述時の最新は3.0.3)
  • info
    • APIの概要情報(タイトルや)を記載する
  • servers
    • APIが稼働しているサーバ情報を指定する
    • コードジェネレータのクライアント側のアクセス先の生成に利用される
  • tags
    • 後述するAPIのpathに紐付けるもの
    • swagger-uiで表示するときにタグにまとめて表示してくれる
  • paths
    • 最も肝となる部分
    • APIのパス、パラメータ、レスポンスの詳細を記述する場所
    • プログラミング言語で表現するなら関数の名前、引数、返り値を定義する場所
    • コードジェネレータに最も影響を与える定義でもある
  • components
    • 再利用される可能性がある部品を定義していく場所
    • プリミティブな型(例えばユーザのID)、パラメータやスキーマの型(例えばユーザ)を定義できる
    • これを記載しなくてもOpenAPIスキーマを定義は可能だが、値の重複が多い状態になる

以下は今回のサンプルで利用する定義だ。
ユーザの取得、更新、作成のエンドポイントを定義したシンプルなもの。
自然言語で書かれているのでざっと見た感じで雰囲気は分かってもらえると思う。
形式はYAMLではなく、JSONも可能。

openapi: 3.0.2
info:
  version: 1.0.0
  title: HelloApiSchema
  description: Practice api schema
  contact:
    name: doriven
    email: doriven@example.com
    url: 'https://example.com'
  license:
    name: MIT
servers:
  - url: http://localhost:{port}
    description: development
    variables:
      port:
        enum:
          - '80'
          - '8080'
        default: '8080'
  - url: http://example.com
    description: example
security:
  - BasicAuth: []
tags:
  - name: user
    description: user api
paths:
  /users:
    post:
      tags:
        - user
      description: Create a new user.
      operationId: createUser
      requestBody:
        description: new user info
        required: true
        content:
          'application:json':
            schema:
              $ref: '#/components/schemas/CreateUserInput'
      responses:
        '200':
          description: hoge
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateUserOutput'
    put:
      tags:
        - user
      description: Update a user.
      operationId: updateUser
      requestBody:
        required: true
        content:
          'application:json':
            schema:
              $ref: '#/components/schemas/UpdateUserInput'
      responses:
        '200':
          description: hoge
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UpdateUserOutput'
  /users/{id}:
    get:
      tags:
        - user
      description: Get a user by id.
      operationId: getUser
      responses:
        '200':
          description: Success by usre
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GetUserOutput'
      parameters:
        - name: id
          in: path
          description: user id
          required: true
          schema:
            type: integer
            format: int64
components:
  schemas:
    user_id:
      type: integer
      format: int64
      example: 1
    email_address:
      type: string
      example: example@example.com
    last_name:
      type: string
      example: 山田
    first_name:
      type: string
      example: 太郎
    birthday:
      type: string
      format: date
      example: 2000-01-01
    address:
      type: string
      example: 東京都新宿区西新宿2丁目8−1
    CreateUserInput:
      type: object
      required:
        - email_address
        - last_name
        - first_name
        - birthday
        - address
      properties:
        email_address:
          $ref: '#/components/schemas/email_address'
        last_name:
          $ref: '#/components/schemas/last_name'
        first_name:
          $ref: '#/components/schemas/first_name'
        birthday:
          $ref: '#/components/schemas/birthday'
        address:
          $ref: '#/components/schemas/address'
    CreateUserOutput:
      type: object
      required:
        - id
        - email_address
        - last_name
        - first_name
        - birthday
        - address
      properties:
        id:
          $ref: '#/components/schemas/user_id'
        email_address:
          $ref: '#/components/schemas/email_address'
        last_name:
          $ref: '#/components/schemas/last_name'
        first_name:
          $ref: '#/components/schemas/first_name'
        birthday:
          $ref: '#/components/schemas/birthday'
        address:
          $ref: '#/components/schemas/address'
    UpdateUserInput:
      type: object
      required:
        - id
      properties:
        id:
          $ref: '#/components/schemas/user_id'
        email_address:
          $ref: '#/components/schemas/email_address'
        last_name:
          $ref: '#/components/schemas/last_name'
        first_name:
          $ref: '#/components/schemas/first_name'
        birthday:
          $ref: '#/components/schemas/birthday'
        address:
          $ref: '#/components/schemas/address'
    UpdateUserOutput:
      # componentsからcomponentsへの$refを使うとgenerator上で空のstructが生成されるのであえて重複して書いている
      # $ref: '#/components/schemas/CreateUserOutput'
      type: object
      required:
        - id
        - email_address
        - last_name
        - first_name
        - birthday
        - address
      properties:
        id:
          $ref: '#/components/schemas/user_id'
        email_address:
          $ref: '#/components/schemas/email_address'
        last_name:
          $ref: '#/components/schemas/last_name'
        first_name:
          $ref: '#/components/schemas/first_name'
        birthday:
          $ref: '#/components/schemas/birthday'
        address:
          $ref: '#/components/schemas/address'
    GetUserOutput:
      # componentsからcomponentsへの$refを使うとgenerator上で空のstructが生成されるのであえて重複して書いている
      # $ref: '#/components/schemas/CreateUserOutput'
      type: object
      required:
        - id
        - email_address
        - last_name
        - first_name
        - birthday
        - address
      properties:
        id:
          $ref: '#/components/schemas/user_id'
        email_address:
          $ref: '#/components/schemas/email_address'
        last_name:
          $ref: '#/components/schemas/last_name'
        first_name:
          $ref: '#/components/schemas/first_name'
        birthday:
          $ref: '#/components/schemas/birthday'
        address:
          $ref: '#/components/schemas/address'

path記法の詳細

この中で最も重要であり複雑なpath記法について説明していく。
以下、上記のスキーマに対して具体的にコメントを添えている。

paths:
  /users: # パス
    post: # HTTP メソッド (get|put|post|deleteなどが指定可能)
      tags: # 関連付けるタグ。複数定義可能
        - user
      description: Create a new user.
      operationId: createUser # コードジェネレータで生成されたクライアント・サーバの関数名に利用される
      requestBody: # パラメータをRequestBodyに定義して渡す場合はこちらを記載する
        description: new user info
        required: true # RequestBodyが必須であることを表している
        content: # RequestBodyの中身の定義をcontent配下に書いていく
          'application:json': # BodyのContentType
            schema: # パラメータの詳細
              $ref: '#/components/schemas/CreateUserInput' # component配下に定義されているのを参照する場合は$refを使用する
              # この場合は.components.schemas.CreateUserInputに定義が書かれている
      responses: # リクエストした結果のレスポンスの定義
        '200': # 返ってくるステータスコード
          description: hoge
          content: # ステータスコードに対応する中身
            application/json: # ContentType
              schema: # レスポンスの詳細定義
                $ref: '#/components/schemas/CreateUserOutput'

他にもgetの場合は以下のように記載可能。
schemaで書かれていることを理解するにはJsonSchemaの定義を知ると、より一層理解が深まる。

...
  /users/{id}: # URIにパラメータを含めることもできる
    get:
      tags:
        - user
      description: Get a user by id.
      operationId: getUser
      responses:
        '200':
          description: Success by user
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GetUserOutput'
      parameters: # URIのパラメータの詳細を定義
        - name: id # URIの{}で囲んだ部分と一致させる必要がある
          in: path # パラメータが含まれている箇所(query, header, path, cookieが選べる)
          description: user id
          required: true
          schema: # パラメータの型定義(JSONSchemaに準拠している)
            type: integer
            format: int64

components記法の詳細

pathsを理解するにはschemaに記載された$refの中身を定義しているcomponentsも合わせて紹介する。
componentsは先程も書いたとおり、重複して利用されるオブジェクトをまとめる箇所だ。
typeやformatの詳細はJsonSchemaを参照のこと。

例えば、POSTであるCreateUserで定義されたcomponentsは具体的にこのように記載されている。

schemas:
  user_id: # user_id という型を定義している
    type: integer
    format: int64
    example: 1 # SwaggerUIで表示される値の具体例
  email_address:
    type: string
    example: example@example.com
  last_name:
    type: string
    example: 山田
  first_name:
    type: string
    example: 太郎
  birthday:
    type: string
    format: date
    example: 2000-01-01
  address:
    type: string
    example: 東京都新宿区西新宿2丁目8−1
  CreateUserInput:
    type: object
    required: # 必須なプロパティを表している
      - email_address
      - last_name
      - first_name
      - birthday
      - address
    properties:
      email_address: # componentsで定義したemail_addressを再利用している
        $ref: '#/components/schemas/email_address'
      last_name:
        $ref: '#/components/schemas/last_name'
      first_name:
        $ref: '#/components/schemas/first_name'
      birthday:
        $ref: '#/components/schemas/birthday'
      address:
        $ref: '#/components/schemas/address'
  CreateUserOutput:
    type: object
    required:
      - id
      - email_address
      - last_name
      - first_name
      - birthday
      - address
    properties:
      id:
        $ref: '#/components/schemas/user_id'
      email_address:
        $ref: '#/components/schemas/email_address'
      last_name:
        $ref: '#/components/schemas/last_name'
      first_name:
        $ref: '#/components/schemas/first_name'
      birthday:
        $ref: '#/components/schemas/birthday'
      address:
        $ref: '#/components/schemas/address'

Swagger Editorというツールがあるので、サンプルのスキーマを埋め込んで見てみる1このAPIの概要がわかる。

モックサーバの構築

OpenAPIスキーマを作成することは出来たので、スキーマ駆動開発の下地は出来た。
ここからはAPIのリクエストを受けてダミーデータを返すGo言語製のモックサーバを作成してみる。

ここで出てくるのが openapi-generator というコードジェネレータ。

openapi-generator とは

名前の通り、OpenAPIに対応したコードジェネレータ。
swagger-codegenというものもあるが、仲違いがあり現状ではopenapi-generatorがオープンソースでコントリビュートされて開発が頻繁にされている状態になっているのでこれからはopenapi-generatorを使っていくのが良い。

Hello openapi-generator

openapi-generatorの環境を含んだDockerが公開されているのでそれを使えば良い。
以下のようにプロジェクトルートをマウントして、出力してやれば良い。

docker run --rm -v {プロジェクトルートパス}:/app openapitools/openapi-generator-cli generate \
    -i {openapiスキーマのyaml or jsonのURL or パスを指定} \
    -l {出力する言語とクライアント or サーバを指定} \
    -o /app/{コードを出力したいパス}

実際のサンプルではプロジェクトルートで以下のようにコマンドを叩いてGo言語製のRestAPIサーバのコードをジェネレートしている。

docker run --rm -v ${PWD}:/app openapitools/openapi-generator-cli generate \
  -c /app/api/go-server-config.json \ # -cはgo-serverで利用可能なオプションを定義したファイルを指定することでコード生成時の挙動を替えてあげることがでる
  -i /app/api/openapi-schema/openapi.yaml \
  -g go-server \ # go だけだとクライアント側のコードが生成される。
  -o /app/server/golang

generatorの-gがどのようなものに対応しているかは公式のこのドキュメントを見ればわかる。
go-serverのドキュメントを見に行くとOpenAPIの対応状況やオプションが記載されているので使用する言語のジェネレータのドキュメントは必ずチェックすること。

出力先ディレクトリには以下のような構造でファイルが置かれる。

server/golang/
├── .openapi-generator
│   └── VERSION # openapi-generatorのバージョン
├── .openapi-generator-ignore # openapiの生成物として除外したいものはこれに指定する
├── Dockerfile
├── README.md
├── go.mod
├── go.sum
├── main.go
└── pkg
    └── openapi
        ├── api.go
        ├── api_default.go
        ├── api_default_service.go
        ├── logger.go
        ├── model_create_user_input.go # pathsのschemaで定義したものと同じ構造のstructが定義されている
        ├── model_create_user_output.go
        ├── model_get_user_output.go
        ├── model_update_user_input.go
        ├── model_update_user_output.go
        └── routers.go

4 directories, 18 files

ダミーデータを返すようにする

出力したばかりだと api_default_service.go は何もせずに500を返すような実装になっている。

// CreateUser -
func (s *DefaultApiService) CreateUser(createUserInput CreateUserInput) (interface{}, error) {
	// TODO - update CreateUser with the required logic for this service method.
	// Add api_default_service.go to the .openapi-generator-ignore to avoid overwriting this service implementation when updating open api generation.
	return nil, errors.New("service method 'CreateUser' not implemented")
}
...

api.go にinterfaceが定義されており、これを実装してmain.goで差し替えることによってダミーデータを返す挙動を実現させていく。

pkg/openapi/api.go
// DefaultApiRouter defines the required methods for binding the api requests to a responses for the DefaultApi
// The DefaultApiRouter implementation should parse necessary information from the http request,
// pass the data to a DefaultApiServicer to perform the required actions, then write the service results to the http response.
type DefaultApiRouter interface {
	CreateUser(http.ResponseWriter, *http.Request)
	GetUser(http.ResponseWriter, *http.Request)
	UpdateUser(http.ResponseWriter, *http.Request)
}


// DefaultApiServicer defines the api actions for the DefaultApi service
// This interface intended to stay up to date with the openapi yaml used to generate it,
// while the service implementation can ignored with the .openapi-generator-ignore file
// and updated with the logic required for the API.
type DefaultApiServicer interface {
	CreateUser(CreateUserInput) (interface{}, error)
	GetUser(int64) (interface{}, error)
	UpdateUser(UpdateUserInput) (interface{}, error)
}

サンプルコードでは pkg/api というディレクトリにモックとなるserviceのコードを実装して配置した。
controller側のコードは特にいじる必要はないので、コードジェネレータから生成されたものをそのまま使う。

pkg/api/mock_service.go
type MockApiService struct {
}

func NewMockApiService() openapi.DefaultApiServicer {
	return &MockApiService{}
}

func (s *MockApiService) CreateUser(createUserInput openapi.CreateUserInput) (interface{}, error) {
	return openapi.CreateUserOutput{
		Id:           1,
		EmailAddress: createUserInput.Address,
		LastName:     createUserInput.LastName,
		FirstName:    createUserInput.FirstName,
		Birthday:     createUserInput.Birthday,
		Address:      createUserInput.Address,
	}, nil
}

func (s *MockApiService) GetUser(id int64) (interface{}, error) {
	return openapi.GetUserOutput{
		Id:           id,
		EmailAddress: "example@example.com",
		LastName:     "田中",
		FirstName:    "太郎",
		Birthday:     "2000-01-01",
		Address:      "東京都新宿区西新宿2丁目8−1",
	}, nil
}

func (s *MockApiService) UpdateUser(updateUserInput openapi.UpdateUserInput) (interface{}, error) {
	return openapi.UpdateUserOutput{
		Id:           updateUserInput.Id,
		EmailAddress: updateUserInput.EmailAddress,
		LastName:     updateUserInput.LastName,
		FirstName:    updateUserInput.FirstName,
		Birthday:     updateUserInput.Birthday,
		Address:      updateUserInput.Address,
	}, nil
}

あとは main.go 側で差し替えを行う。

	      log.Printf("Server started")

-       DefaultApiService := openapi.NewDefaultApiService()
+       mockApiService := api.NewMockApiService()
        DefaultApiController := openapi.NewDefaultApiController(mockApiService)

        router := openapi.NewRouter(DefaultApiController)

ここまでくれば go run させてみればモックサーバが立ち上がる。
curlなどで動作確認した結果は以下。

⟩ go run main.go
2020/03/08 16:59:46 Server started
2020/03/08 17:00:20 GET /users/1 GetUser 456.134µs
⟩ curl -sf 'localhost:8080/users/1' | jq .
{
  "id": 1,
  "email_address": "example@example.com",
  "last_name": "田中",
  "first_name": "太郎",
  "birthday": "2000-01-01",
  "address": "東京都新宿区西新宿2丁目8−1"
}

これでモックサーバが完成した。
実際の開発においても、コードジェネレータしたコードには極力触らず(main.goは仕方ないとして)に拡張によって挙動を変えることでOpenAPIスキーマの定義を守れるだろう。

go-serverのPreflight対応

これは私が詰まった部分なので記述するが、go-serverのコードジェネレータはAPIのプリフライト(Preflight)リクエストに対応しておらず、ステータス405が返ってきて使えなかった。
今回はPUTもAPIのHTTPメソッドとして利用するのでPreflightが使えないとクライアント側で困ってしまう。

下記のようにgo-serverのオプションでCORSのオプションを有効にしたものの、それでは駄目だった。
これだとGET, POSTのCORSしか対応してくれない。

{
  "sourceFolder": "pkg/openapi",
  "featureCORS": true
}

理由としてはmain.goで生成しているtopのrouterでCORSを許容していないため。
オプションではRouter生成時にroute(path)毎にCORSを許可するようにはしてくれるものの、Router側にはCORSが許可されない。
そこで生成されたrouterに対して、CORSメソッドを指定し、引数として許可したいメソッドと追加のヘッダーであるContent-Typeを指定する修正をmain.goに加えた。2

-       log.Fatal(http.ListenAndServe(":8080", router))
+       log.Fatal(http.ListenAndServe(":8080", handlers.CORS(
+               handlers.AllowedMethods([]string{"GET", "PUT", "POST", "DELETE"}),
+               handlers.AllowedHeaders([]string{"Content-Type"}),
+       )(router)))

クライアントコードの実装

続いてクライアント側のコードもopenapi-generatorを使用してWebブラウザで利用するTypescriptクライアントを生成していく。

docker run --rm -v ${PWD}:/app openapitools/openapi-generator-cli generate \
  -c /app/api/go-server-config.json \
  -i /app/api/openapi-schema/openapi.yaml \
  -g typescript-fetch \
  -o /app/client/ts/src/api-client

すると以下のようなコードが生成されている。
これはライブラリと捉えて後はお手製のTypeScriptのコードから利用すれば良い。
クライアントからはこの src/api-client をimportして利用する。

└── src
    └── api-client
        ├── apis
        │   ├── UserApi.ts
        │   └── index.ts
        ├── index.ts
        ├── models
        │   ├── CreateUserInput.ts
        │   ├── CreateUserOutput.ts
        │   ├── GetUserOutput.ts
        │   ├── UpdateUserInput.ts
        │   ├── UpdateUserOutput.ts
        │   └── index.ts
        └── runtime.ts

コードジェネレータしたコードの利用サンプルは以下。
ボタンを押したらFetchAPIでリクエストしてレスポンスの結果をconsoleに吐き出すという非常に単純なものだ。
クライアントがするべきことはコードジェネレータで生成されたDefaultApiというクライアントクラスを使用して○○Requestを作って引数に渡してやるだけで良い。
(勉強がてら私はReactを使ったが、サンプル実装だけなら純粋にTypeScriptだけを使えばいいと思う)

App.tsx
import * as React from "react";
import {GetUser} from "./components/GetUser";
import {UpdateUser} from "./components/UpdateUser";
import {CreateUser} from "./components/CreateUser";

export default class App extends React.Component<any, any> {
    render() {
        return (
            <React.Fragment>
                <GetUser/>
                <UpdateUser/>
                <CreateUser/>
            </React.Fragment>
        );
    }
};
GetUser.tsx
import * as React from "react";
import {DefaultApi, GetUserOutput} from "../api-client";

export let GetUser = () => (
    <button onClick={() => {
        const c: DefaultApi = new DefaultApi();
        c.getUser({id: 1}).then((v: GetUserOutput) => {
            console.log(v);
        });
    }}>GetUser</button>
);
CreateUser.tsx
import * as React from "react";
import {CreateUserOutput, CreateUserRequest, DefaultApi} from "../api-client";

export let CreateUser = () => (
    <button onClick={() => {
        const c: DefaultApi = new DefaultApi();
        const input: CreateUserRequest = {
            createUserInput: {
                address: '東京都新宿区西新宿2丁目8−1',
                birthday: new Date(),
                emailAddress: 'taro@example.com',
                lastName: '田中',
                firstName: '太郎',
            },
        };
        c.createUser(input).then((v: CreateUserOutput) => {
            console.log(v);
        });
    }}>CreateUser</button>
);
UpdateUser.tsx
import * as React from "react";
import {DefaultApi, UpdateUserOutput, UpdateUserRequest} from "../api-client";

export let UpdateUser = () => (
    <button onClick={() => {
        const c: DefaultApi = new DefaultApi();
        const input: UpdateUserRequest = {
            updateUserInput: {
                id: 1,
                address: '静岡県駿東郡小山町桑木',
                birthday: new Date('2020-03-08'),
                emailAddress: 'kin_taro@example.com',
                lastName: '',
                firstName: '太郎',
            },
        };
        c.updateUser(input).then((v: UpdateUserOutput) => {
            console.log(v);
        });
    }}>UpdateUser</button>
);

以下のようなレスポンスがconsole.log上に返ってくる。

# GetUser
{
  "id": 1,
  "emailAddress": "example@example.com",
  "lastName": "田中",
  "firstName": "太郎",
  "birthday": "2000-01-01T00:00:00.000Z",
  "address": "東京都新宿区西新宿2丁目8−1"
}

# CreateUser
{
  "id": 1,
  "emailAddress": "東京都新宿区西新宿2丁目8−1",
  "lastName": "田中",
  "firstName": "太郎",
  "birthday": "2020-03-08T00:00:00.000Z",
  "address": "東京都新宿区西新宿2丁目8−1"
}

# UpdateUser
{
  "id": 1,
  "emailAddress": "kin_taro@example.com",
  "lastName": "金",
  "firstName": "太郎",
  "birthday": "2020-03-08T00:00:00.000Z",
  "address": "静岡県駿東郡小山町桑木"
}

クライアント側はこのモックサーバの値を利用して、開発をしていくことが可能でAPIのスキーマが変わってもコードジェネレータを使用することで入出力部分の最小限の変更だけで済ませることができる。

実装していて困った点とか

  • 未使用な変数や関数が定義されているので、TypeScriptの設定を厳密なものしているとコケる
    • tsconfigの設定はゆるくして一旦は対応した
    • コード側にはtslint ignoreが設定されているのでtslintで厳密さは担保すれば良さそうな気はしている
  • クライアントが生成するサーバーのURLは一番上が利用される
    • マルチサーバを設定しているならモック用の設定は一番上に持ってくる必要がある

クライアント実際に使ってみた個人的な所感

コードジェネレータでのドキュメントと実装の一致が可能なのは魅力的に感じた。
一方で最初に導入するハードルはそこそこ高いと感じた(以下、理由)ので外部向けのAPIの仕様定義としては良いが、内部利用向けのAPIの定義に使うかと聞かれると微妙な気持ちになる。

スキーマ定義がそれなりに大変

  • swagger-editorを使ってもそんなにスキーマ定義の作成が楽になったとは思えなかった
  • 結局は手元のエディタを開いて地道に定義を書いていくことになると思う(もちろん、IDEのプラグインのサポートなどは使ったりしたが)

重要なコードジェネレータの質が言語によってまちまち

  • openapi-generatorのクライアント・サーバのコード生成は言語毎にコントリビューターがそれぞれ作成している
  • そのためOpenAPIの仕様を実装できているかはその言語のコントリビュートの活発さや質に左右される
  • 例えば今回のgo-serverの微妙な点としてPreflightへ対応していなかったり、必須パラメータの型がポインタではないためパラメータが送られていないのかどうかの判断が難しいというのがあったりする
  • なので、仕事で使う場合には選択した言語のコントリビュートをするくらいの気持ちで使っていかないと厳しい(自前でコード生成を定義するという技もあるが)

コードジェネレータのコントリビュートをするためにJavaとその周辺ツールを覚える必要がある

  • openapi-generatorはJavaで実装されている
  • コード生成もJavaのmustacheというテンプレートエンジンが使われている
  • そのほかにもJavaのツールを覚える必要がある
  • つまり、自分たちが使いやすくしたかったらクライアントの言語 + サーバの言語だけでなくJavaを理解する必要がありそこそこに導入コストが高いと思う

まとめ

ドキュメントと実装の一致をさせることが可能であり、API仕様書の生成を楽に行えるという嬉しさはあるものの、使うにはそれなりに色々な知識が要求されるということが分かった。
実際にOpenAPI3とopenapi-generatorを使った簡単なモックサーバの作成とクライアントサンプル実装を行って理解を深められたのは大きいので、読者も実際に手を動かしてみて導入の判断を決めてもらえればと思う。

  1. SwaggerUIをURLパラメータから指定できるようにしてくれた方のツールを利用

  2. PRを出そう考えたが筆者はJavaに明るくなくテンプレートエンジンのmustacheを理解することやopenapi-generatorの仕様を理解しないとPRを出せなさそうだったので早々に諦めた。

29
19
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
29
19

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?