ミツモア Tech blog

「ミツモア」を運営する株式会社ミツモアの技術ブログです

API Gatewayのswagger.jsonをNestJSと組み合わせて自動生成する

こんにちは、株式会社ミツモアの坂本です

この記事はミツモアアドベントカレンダー2024 2日目の記事です。

本記事ではプロワン内で実施している API Gateway の設定を自動化するためのJSON自動生成について紹介します。

API Gateway と OpenAPI

プロワンでは外部から利用できるAPIをいくつか提供しており、AWSのAPI Gatewayを用いて認証や流量制御などを行っています。

API Gatewayは安全にAPIを公開できるサービスですが、そのバックエンドにある実際のAPIサーバーに接続するための設定が必要です。Web UIやaws-cliを利用して設定を変更することが可能ですが、手動で維持管理をするには限界があります。

エンドポイントを多数提供している場合、手動で管理すると設定漏れなどのミスが発生する高くなってしまいます。

API GatewayはOpenAPI形式で記述されたjsonの読み込みをサポートしており、一括でAPIのセットアップを行えます。

OpenAPI形式はステータスコードやパラメーターなど基本的な項目に加え、カスタムプロパティを記述することが可能です。このカスタムプロパティを利用してAPI Gatewayの設定に必要な情報を付与することで、JSONを読み込ませてデプロイするだけで反映作業が完了する状態を実現します。

公式ドキュメントに記載されている項目を参考にすれば、基本的にほとんど全ての項目が設定可能です。(多分)

https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/api-gateway-swagger-extensions-integration.html

プロワンではAPI Gatewayに定義されたエンドポイントと一対一で対応するAPIを裏側に置いています。

各バックエンドAPIの定義からOpenAPIのJSONを自動で生成できれば、実際のAPI仕様と差分が生まれることを防げます。

NestJS + Swagger

プロワンではNestJSをサーバーフレームワークに利用しており、 @nestjs/swagger と組み合わせることでOpenAPIのJSONを自動で生成することができます。

https://docs.nestjs.com/openapi/introduction

この機能を利用してAPI Gatewayに必要なJSONを生成していきます。

API Gatewayで必要な情報をControllerに対して指定すると以下のようになります。

@Controller()
class TestController {
    @Get('/posts/:id')
    @ApiHeaders([
    { name: 'x-api-key', required: true },
  ])
  @ApiNotFoundResponse()
  @ApiSecurity('api_key')
  @ApiExtension('x-amazon-apigateway-integration', {
    type: 'HTTP',
    httpMethod: 'GET',
    uri: '/posts/{id}',
    connectionType: 'INTERNET',
    requestParameters: {
      'integration.request.header.x-api-key': 'method.request.header.x-api-key',
      'integration.request.path.id': 'method.request.path.id'
    },
    passthroughBehavior: 'WHEN_NO_TEMPLATES',
    timeoutInMillis: 29000,
    cacheKeyParameters: [],
    responses: {
      '200': {
        statusCode: '200',
      },
      '404': {
        statusCode: '404',
        selectionPattern: '404',
      },
    },
  })
  async getPost(@Params('id') id: string) {
    ...
  }
}

カスタムデコレーターが欲しい

合計5つのデコレーターを記述する必要があり、これをエンドポイントごとに記述するのは大変ですし、漏れも出やすくなります。実際はこれ以外にも記述している項目があり、より複雑です。

そこでカスタムデコレーターを作成して一発で記述していきます。

@ApiDefinition({
  path: '/posts/:id',
  method: 'GET',
})
async getPost(@Params('id') id: string) {
  ...
}

はい、とってもシンプルになりました。

固定値の部分は内部で各デコレーターをまとめているだけなので省略しますが、動的な部分に絞ってピックアップしていきます。

GET/POSTなどのメソッド指定と、そのパスが変数部分になります。

注意点

  1. NestJSのパス変数とAPI Gatewayのパス変数の記述方法が異なる

NestJSでは /posts/:id ですが、API Gatewayでは /posts/{id} とする必要があります

  1. API Gatewayから裏側のAPIへ流す時のパラメーターのマッピングが必要

バックエンドAPIへもパラメーターを流すために integration.request.path.xxxmethod.request.path.xxx をマッピングさせる必要があります。

  1. Postで201を返している場合、ワークアラウンドが必要

@nestjs/swagger の実装が原因で @Post() もしくは @HttpCode() がデコレーターとして記述されていないと201を返す挙動をしてくれないため、POSTメソッドの時のみ別で明示的に指定してあげる必要があります

@ApiDefinition({
  path: '/posts',
  method: 'POST'
})
@Post('/posts') // この記述が必要
async createPost() {
  ...
}

@nestjs/swagger の該当箇所: https://github.com/nestjs/swagger/blob/5f405315a35f76a8359fc88ea1e0dbaaec21dd7a/lib/plugin/visitors/controller-class.visitor.ts#L479-L501

以上をまとめると以下のような実装となります。descriptionやsummaryなど他の項目を足したい場合もこのデコレーターを拡張していくと統一した記述や使い方ができます。

type ApiDefinitionParam = {
  method: 'GET' | 'POST' | 'PUT' | 'DELETE'
  path: string
}

export const ApiDefinition = (params: ApiDefinitionParam): MethodDecorator => {
  const { method, path } = params

 ...

  const uri = 'https://example.com' + path

  /************ APIのパスからパラメータを取得 **************/
  const pathParameters = uri.match(/:([^/]+)/g)?.map((p) => p.slice(1)) ?? []
  const requestPathParameters = pathParameters.reduce(
    (acc, param) => {
      acc[`integration.request.path.${param}`] = `method.request.path.${param}`
      return acc
    },
    {} as Record<string, string>,
  )

  /*********** AWS API Gateway 用の Swagger の拡張 *********/
  const extension = ApiExtension('x-amazon-apigateway-integration', {
    type: 'HTTP',
    httpMethod: method,
    uri: uri.replace(/:([^/]+)/g, '{$1}'),
    connectionType: 'INTERNET',
    requestParameters: {
      'integration.request.header.x-api-key': 'method.request.header.x-api-key',
      ...requestPathParameters,
    },
    passthroughBehavior: 'WHEN_NO_TEMPLATES',
    timeoutInMillis: 29000,
    cacheKeyParameters: [],
    responses: {
      '200': {
        statusCode: '200',
      },
      '404': {
        statusCode: '404',
        selectionPattern: '404',
      },
    },
  })

  /*************** エンドポイント & メソッド ****************/
  let methodDecorator: MethodDecorator | undefined = undefined
  switch (method) {
    case 'GET': {
      methodDecorator = Get(path)
      break
    }
    case 'POST': {
      /**
       * NOTE: Use @Post directly in order to return 201 response
       * Postは @nestjs/swagger の実装上 typescript のデコレーターとして認識されないと201になってくれないので、現行と合わせるために @Post() 自体残す必要がある
       * @see https://github.com/nestjs/swagger/blob/5f405315a35f76a8359fc88ea1e0dbaaec21dd7a/lib/plugin/visitors/controller-class.visitor.ts#L479-L501
       */
      break
    }
    case 'PUT': {
      methodDecorator = Put(path)
      break
    }
    case 'DELETE': {
      methodDecorator = Delete(path)
      break
    }
    default:
      throw new Error('Unsupported HTTP method')
  }

  return applyDecorators(
    notFoundResponse,
    headers,
    security,
    extension,
    methodDecorator
  )
}

生成結果

前述の設定を行うと、以下のようなJSONが生成されます。(一部省略してあります)

これをAPI GatewayのWeb UIから読み込ませることで面倒なRequest Integrationのセクションの設定を自動化することができます。

{
  "openapi": "3.0.0",
  "paths": {
    "/api/posts/{id}": {
      "get": {
        "operationId": "GetPost",
        "x-amazon-apigateway-integration": {
          "type": "HTTP",
          "httpMethod": "GET",
          "uri": "https://example.com/api/posts/{id}",
          "connectionType": "INTERNET",
          "requestParameters": {
            "integration.request.header.x-api-key": "method.request.header.x-api-key",
            "integration.request.header.x-api-version": "method.request.header.x-api-version",
            "integration.request.path.id": "method.request.path.id"
          },
          "passthroughBehavior": "WHEN_NO_TEMPLATES",
          "timeoutInMillis": 29000,
          "cacheKeyParameters": [],
          "responses": {
            "200": {
              "statusCode": "200"
            },
            "400": {
              "statusCode": "400",
              "selectionPattern": "400"
            },
            "404": {
              "statusCode": "404",
              "selectionPattern": "404"
            },
            "500": {
              "statusCode": "500",
              "selectionPattern": "500"
            }
          }
        },
        "summary": "",
        "description": "",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "記事ID",
            "example": "1",
            "schema": {
              "nullable": false,
              "type": "string"
            }
          },
          {
            "name": "x-api-key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Post"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotFoundErrorResponse"
                }
              }
            }
          }
        },
        "tags": [],
        "security": [
          {
            "api_key": []
          }
        ]
      }
    }
  }
}

まとめ

この記事では、項目数の多いAPI Gatewayの設定をJSONインポートにより一発解決する方法と、そのJSONの自動生成について紹介しました。

  • プロワンではAPI Gatewayを用いてAPIを公開している。
  • API GatewayはOpenAPI形式のJSONのインポートをサポートしている。
  • カスタムプロパティを利用することでほとんどの設定を指定可能。
  • NestJSとSwaggerを組み合わせることでJSONを自動生成することが可能。
  • 項目が多いのでカスタムデコレーターを作成した。

AWS CLIからでもJSONインポートは可能なので、CIと組み合わせることでAPIのデプロイ自動化も可能です。もちろんAPI定義から直接CLIのコマンドを生成することもできるでしょうが、OpenAPIという汎用的なフォーマットを経由することでわかりやすくメンテナンスしやすい仕組みになっているため、そちらがおすすめです。