Swaggerを使ったAPIドキュメントの作成と、バックエンドとフロントエンド間の連携

こんにちは。LINE Growth Technology福岡開発室でサーバーサイドエンジニアをしている中村です。

この記事では担当していたプロジェクトで実施した、Swaggerを使ったAPIドキュメントの作成と、BE(バックエンド)とFE(フロントエンド)間の連携について紹介します。

プロジェクトの説明

はじめに、今回LINE Growth Technology福岡開発室(以下「GT」)がシステムの設計開発を担当した、LINE公式アカウント審査ツールのプロジェクトについて説明します。

LINE公式アカウント審査ツールとは、LINE公式アカウントで認証済アカウントが申請された際に、申請内容が適切であるか審査するシステムです。
このシステムは、LINE公式アカウントが利用されている各国に存在する審査パートというチーム(以下「審査チーム」)によって利用され、日本以外の国家も含め、一日あたり平均約1,000件の申請を審査します。

プロジェクトの開発は大きくわけて、審査チームが使用するWEB画面を提供するFEと、WEB画面及び他システムにAPIを提供するBEが存在します。
今回私はBE担当としてこのプロジェクトに参加しました。

プロジェクトの課題、Swaggerを使用した経緯

先述の通り、BEはAPIを用いてFE及び他システムと連携するため、APIの作成にあたり次の課題がありました。

  • APIドキュメントを作る必要がある
  • APIドキュメントを手動管理するコストをかけられない
  • FEとBEの開発時期が重なっている
  • FEのスムーズな開発のため、素早くAPIを定義しなければならない

以上の課題を解決するために、手動ではなく自動でAPIドキュメントを作成する方法を調べたところ、Swaggerで実現可能であることがわかりました。
SwaggerとはOAS(OpenAPI Specification。REST APIの仕様を定義するフォーマット。)に基づくツールセットであり、以下の機能で構成されます。

  • Swagger Editor: OASを作成する
  • Swagger UI: OASからHTMLドキュメントを作成する
  • Swagger Codegen: OASから各言語のコードを作成する

Swaggerの導入

1. OASの設定
GTのBE開発では、主にJavaのSpring Frameworkを使用しています。
そしてSpring Foxというサードパーティのライブラリを組み合わせることで、Spring FrameworkのコードからOASを作成することができます。
http://springfox.github.io/springfox/

ソースコード上のAPIエンドポイントやリクエスト・レスポンスモデルにSpring Foxのアノテーションを付与することで、OASの内容を設定します。

OAS設定 

@Configuration
@EnableSwagger2
public class SwaggerConfig {
 
    @Bean
    public Docket docket() {
        return new Docket(DocumentationType.SWAGGER_2)
                .select()
                .apis(RequestHandlerSelectors.any())
                .paths(PathSelectors.regex("/api.*"))
                .build()
                .useDefaultResponseMessages(false);
    }
}
 
@Data
@ApiModel(description = "Pet")
public class Pet {
 
    @ApiModelProperty(value = "ID", example = "1")
    private int id;
 
    @ApiModelProperty(value = "名前", example = "Name")
    private String name;
}
 
@RestController
@RequestMapping("/api/pet")
public class PetController {
 
    @ApiOperation("Petを取得")
    @ApiResponse(code = 200, message = "Pet", response = Pet.class)
    @GetMapping("/{id}")
    public Pet getPet(
            @ApiParam(value = "PetのID", required = true, example = "1") @PathVariable int id) {
        // TODO: 実装
        return new Pet();
    }
 
    @ApiOperation("Petを作成")
    @PostMapping
    public void createPet(@ApiParam(value = "Pet", required = true) Pet pet) {
        // TODO: 実装
    }
 
    @ApiOperation("Petを更新")
    @PutMapping("/{id}")
    public void updatePet(@ApiParam(value = "PetのID", required = true, example = "1") @PathVariable int id,
                          @ApiParam(value = "Pet", required = true) @RequestBody Pet pet) {
        // TODO: 実装
    }
 
    @ApiOperation("Petを削除")
    @DeleteMapping("/{id}")
    public void deletePet(@ApiParam(value = "PetのID", required = true, example = "1") @PathVariable int id) {
        // TODO: 実装
    }
}

これでアプリケーションを起動してhttp://localhost:8080/v2/api-docsにアクセスすると、OASを確認できるようになりました。

2. OASファイルを作成
WEB上で確認できるようになったOASをGithubで管理するため、OASをファイルに保存できるようにします。
OASを確認するにはアプリケーションを起動中でなければいけないので、OASをダウンロードしてファイルに保存する処理をテストコードとして書きます。

OASファイル作成 

@SpringBootTest
@ActiveProfiles("test")
public class GenerateOasFile {
 
    @Autowired
    protected WebApplicationContext context;
 
    @Test
    public void generateOasFile() throws Exception {
        MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
 
        mockMvc.perform(get("/v2/api-docs").contentType(MediaType.APPLICATION_JSON))
                .andDo(result -> FileUtils.writeStringToFile(
                        new File("build/docs/oas.json"),
                        new String(result.getResponse().getContentAsByteArray(), StandardCharsets.UTF_8)));
    }
}

このテストを実行すると、OASをファイルに保存できます。

OASファイル 

$ ./gradlew test --tests GenerateOasFile
 
$ cat bulid/docs/oas.json
{
    "swagger": "2.0",
    "info": {
        "description": "Api Documentation",
        "version": "1.0",
        "title": "Api Documentation",
        "termsOfService": "urn:tos",
        "contact": {},
        "license": {
            "name": "Apache 2.0",
            "url": "http://www.apache.org/licenses/LICENSE-2.0"
        }
    },
    "host": "localhost",
    "basePath": "/",
    "tags": [
        {
            "name": "pet-controller",
            "description": "Pet Controller"
        }
    ],
    "paths": {
        "/api/pet": {
            "post": {
                "tags": [
                    "pet-controller"
                ],
                "summary": "Petを作成",
                "operationId": "createPetUsingPOST",
                "consumes": [
                    "application/json"
                ],
                "produces": [
                    "*/*"
                ],
                "parameters": [
                    {
                        "name": "id",
                        "in": "query",
                        "description": "ID",
                        "required": false,
                        "type": "integer",
                        "format": "int32",
                        "x-example": 1
                    },
                    {
                        "name": "name",
                        "in": "query",
                        "description": "名前",
                        "required": false,
                        "type": "string",
                        "x-example": "Name"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                },
                "deprecated": false
            }
        },
        "/api/pet/{id}": {
            "get": {
                "tags": [
                    "pet-controller"
                ],
                "summary": "Petを取得",
                "operationId": "getPetUsingGET",
                "produces": [
                    "*/*"
                ],
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "description": "PetのID",
                        "required": true,
                        "type": "integer",
                        "format": "int32",
                        "x-example": 1
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "schema": {
                            "$ref": "#/definitions/Pet"
                        }
                    }
                },
                "deprecated": false
            },
            "put": {
                "tags": [
                    "pet-controller"
                ],
                "summary": "Petを更新",
                "operationId": "updatePetUsingPUT",
                "consumes": [
                    "application/json"
                ],
                "produces": [
                    "*/*"
                ],
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "description": "PetのID",
                        "required": true,
                        "type": "integer",
                        "format": "int32",
                        "x-example": 1
                    },
                    {
                        "in": "body",
                        "name": "pet",
                        "description": "Pet",
                        "required": true,
                        "schema": {
                            "$ref": "#/definitions/Pet"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                },
                "deprecated": false
            },
            "delete": {
                "tags": [
                    "pet-controller"
                ],
                "summary": "Petを削除",
                "operationId": "deletePetUsingDELETE",
                "produces": [
                    "*/*"
                ],
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "description": "PetのID",
                        "required": true,
                        "type": "integer",
                        "format": "int32",
                        "x-example": 1
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                },
                "deprecated": false
            }
        }
    },
    "definitions": {
        "Pet": {
            "type": "object",
            "properties": {
                "id": {
                    "type": "integer",
                    "format": "int32",
                    "example": 1,
                    "description": "ID"
                },
                "name": {
                    "type": "string",
                    "example": "Name",
                    "description": "名前"
                }
            },
            "title": "Pet",
            "description": "Pet"
        }
    }
}

3. OASファイルの自動作成とAPIドキュメント公開
OASファイルをGithubで管理します。
今回はCircleCIを使用して、APIが更新されるようなPull Requestをmasterブランチにmergeするとき、自動でOASファイルを作成してpushするようにしました。

CircleCI 

version: 2
jobs:
  update_doc:
    docker:
      - image: openjdk:11
        name: jdk
    steps:
      - checkout
      - run: ./gradlew test --tests GenerateOasFile
      - run: bash .circleci/update-oas.sh
 
workflows:
  version: 2
  update_doc:
    jobs:
      - update_docs:
          filters:
            tags:
              only: /.*/
            branches:
              only: master

update-oas.sh 

#!/bin/bash
 
git pull origin master
 
SRC_DIR="build/docs"
DST_DIR="docs"
DIFF=$(diff -qr "$SRC_DIR" "$DST_DIR")
if [ -z "$DIFF" ]; then
    exit 0
fi
 
rm -rf "$DST_DIR"
cp -r "$SRC_DIR" "$DST_DIR"
 
git add "$DST_DIR"
git config user.email "email@example.com"
git config user.name "name"
git commit -m "Update docs"
git push origin master

そして、作成したOASファイルとSwagger UIをGithub Pagesに公開することで、WEB上でAPIドキュメントをHTMLとして閲覧できるようになりました。

4. OASを通じてFEとBEの連携
OASからFEのコードを作成します。
FEはTypeScriptを使ってWeb画面を開発していますが、サードパーティライブラリのswagger-to-tsを使用することで、OASからTypeScript型定義を作成できます。

swagger-to-ts 

$ npx @manifoldco/swagger-to-ts@1 oas.json --output schema.ts
npx: 78個のパッケージを5.22秒でインストールしました。
 oas.json -> schema.ts [1ms]
 
$ cat schema.ts
/**
 * This file was auto-generated by swagger-to-ts.
 * Do not make direct changes to the file.
 */
 
declare namespace OpenAPI2 {
  export interface Pet {
    /**
     * ID
     */
    id?: number;
    /**
     * 名前
     */
    name?: string;
  }
}

効果、メリット

Swaggerを導入することで、最初に挙げたプロジェクトの課題を解決することができました。

APIドキュメントはソースコードから作成されるようになり、CircleCIで常に最新の状態が維持されるため、手動で管理する必要はなくなりました。
APIドキュメントはFEとBEのコミュニケーションの基準になるので、それが正しく管理されていることで円滑なコミュニケーションを取ることもできました。

FEとBEの開発期間が重なっているため素早くAPIを定義しなければいけない件についても、BEがAPIの内部実装をする前にエンドポイントやリクエスト・レスポンスモデルの設定だけ済ませてしまえばOASを作成できるため、FEはBEのAPI実装を待たずスピーディに開発を進められたと感じます。

そして予定外だったメリットは、OASからType Script型定義を作成できたことです。
Type Script型定義はFEにとって大変な手間だったようで、それをOASから作成することでFEの負担を大きく軽減できたとのことです。

反省点、新たな課題

以上の有効な効果を得られたましたが、いくつかの反省点もありました。

ます、Spring Foxのアノテーションです。
簡単なOASを作成するには最低限のアノテーションを設定すれば良いのであまり手間がかからないのですが、詳細のものを作ろうとするとアノテーションを多く設定する必要があり多少の手間がかかりました。
また、導入初期はアノテーションの種類を完全に把握できていなかったことも一因です。
しかし慣れてしまえば簡単な仕組みですし、一部を設定すればあとはコピー&ペーストできる部分も多いため、経験と事前学習で対応可能かと思います。

そして、OASから作成するTypeScript型定義が完全ではないことです。
swagger-to-tsを使用してTypeScript型定義を作成しましたが、定義されるのはリクエスト・レスポンスモデルの型だけで、APIのURLパラメータは定義されませんでした。
そのため、いくつかのURLパラメータは手動で型定義することになりました。
型定義を完全に自動作成するのであれば、swagger-to-tsの詳細な使用方法の調査や、swagger-to-ts以外のツールを使用することも検討する必要があります。

最後に

以上、APIドキュメントの作成と、FEとBE間の連携という課題に対する、Swagger導入の体験について書かせていただきました。

ドキュメントは作成・管理するコストに対してリターンが小さい上に、正しく管理されていなければ誤った記述が残されて混乱を招くため敬遠されがちです。
それでも、ソースコードからの作成や自動管理など工夫することで、そのハードルをある程度は下げられたと感じます。

新たな課題はありますが、コスト以上のリターンを得ることができましたので、他のプロジェクトでも是非導入をすすめたいと思います。