My Brain is Open.

思いついたことを適当に列列と

外部連携APIのモックサーバをSwaggerを利用して作る

外部APIとの連携をしたい。でも同時並行で実装中でモックすら無い。どうしよう…。

様々な関係会社の方々と連動して仕様を定めながら開発を進めていかなければならない場合、実際の接続を確認しながら実装を作りたいけど接続先のサーバがない、という状況が起こると思います。 簡単なものであれば、 Mockable.ioなどを使ってシンプルに応答を返すこともできるのですが、実際に開発やテストを行うには幾つかの機能が求められます。

  • 特定条件のリクエストに対してエラー応答したい、またはタイムアウトを発生させたい
  • 特定条件のリクエストに対して様々なパターンの正常応答を返したい
  • 開発の進行に伴い、実装に合わせてモックの動きもバージョン管理したい

このようなニーズに答えるにはMockable.ioなどのサービスだけでは難しく、API開発元に開発中バージョンのモックを作ってもらうか、自らモックサーバを実装する必要が出てきます。

できれば開発元にモックやクライアントライブラリの作成をお願いしたいと思いつつ、オープンなAPIでなければそのようなものは作られないでしょう。 しかし、自作するにしてもスクラッチでWebアプリケーションを作って本格的にAPIサーバを作るのは辛いですよね。(そこまで辛くなければ良いのですが…) そこで、なんらかのツールを使ってAPIモックサーバを作ることを考えてみましょう。

APIドキュメンテーションツールの選択肢

そもそも、外部との連携を行うためのHTTPベースのAPI(Application Programming Interface)を作る場合は、クライアントとなるシステムと正しいやり取りを行うために 正確なドキュメント が必要です。 エクセルなどで頑張る人もいると思うのですが、これらを支援するツールが APIドキュメンテーションツール です。様々なツールがあり、それぞれAPIの仕様を記述するのに決められたフォーマットを持っています。 以下に代表的なフォーマットを列挙します。

  • Swagger
  • RAML
  • API Blueprint ( Apiary.io )
  • JSON Schema

これらのフォーマットに共通した特徴として、

  • HTTPリクエストのメソッドとエンドポイントURL
  • 各エンドポイントで使用するパラメータ名とデータフォーマット
  • JSON(またはXML)の構造

などを記述できるようになっており機械的に処理ができるようになっているので、ツールを使えばHTMLのAPIドキュメントが出力できますし、自動的にクライアント実装を生成してWebブラウザでAPIのリクエストを実際に発行したりすることができます。 接続先を変更することで実際に稼働しているAPIとも接続ができ、「動作するドキュメント*1」としてとても有用です。

それと同時に、各言語向けのサーバまたはクライアントの実装テンプレートを出力するジェネレータが存在します。 あくまでもテンプレートなので必要に応じて個別に実装を作り込む必要がありますが、全てを実装しなければならなかったことを考えるとかなり楽になります。

このフォーマットの中で現時点で最も人気なのが Swagger で、RESTful APIドキュメンテーションフォーマットを策定するOpen API Initiativeのベースとなるフォーマットとして指定されています。*2

今回は Swagger + Node.js によるモックサーバの作り方のサンプルを書いてみます。

Swagger-Editor によるドキュメント作成

Swaggerの定義を書くのに便利なツールはSwagger-Editorです。

Swagger-Editor

f:id:mather314:20161028193214p:plain
swagger-editorの画面

初回アクセス時にはテンプレートが表示されており、執筆時点ではUber APIをサンプルにAPI定義が書かれています。

左画面にYAML形式のAPI定義を記述すると、右画面のドキュメントが自動的に更新されていき、内容に間違いがある場合はエラーを表示して指摘してくれます。

YAMLの記述方法について

SwaggerのAPI定義は Swagger Specification を参照することになりますが、全体的にちょっとわかりにくいかもしれません。

長いですが、以下はユーザのCRUDを定義したサンプルコードです。

### メタ情報
swagger: '2.0'
info:
  title: Sample API
  description: Sample of Simple API
  version: "1.0.0"
host: sample.example.com
schemes:
  - https
# 全てのAPIのペースになるパス。 /v1/users のようになる。
basePath: /v1
produces:
  - application/json
consumes:
  - application/json
### エンドポイント一覧
paths:
  /users:
    get:
      summary: ユーザ一覧
      parameters:
        - name: name
          in: query
          description: ユーザ名
          type: string
          required: false
      responses:
        200:
          description: ユーザ一覧
          schema:
            type: array
            items:
              $ref: "#/definitions/User"
          examples:
            "application/json":
              - id: 1
                name: hoge
                email: hoge@example.com
    post:
      summary: ユーザ追加
      parameters:
        - name: user
          in: body
          description: ユーザ情報
          schema:
            $ref: "#/definitions/UserRequest"
      responses:
        201:
          description: Created
          schema:
            $ref: "#/definitions/Success"
        default:
          description: Error
          schema:
            $ref: "#/definitions/Error"
  /user/{id}:
    get:
      summary: ユーザ情報
      description: |
        長文はこちらに書く
      parameters:
        - name: id
          in: path
          description: ユーザID
          required: true
          type: integer
          format: int32
      responses:
        200:
          description: 商品一覧の配列
          schema:
            $ref: '#/definitions/User'
        default:
          description: エラー
          schema:
            $ref: '#/definitions/Error'
    put:
      summary: ユーザ更新
      description: |
        長文はこちらに書く
      parameters:
        - name: id
          in: path
          description: ユーザID
          required: true
          type: integer
          format: int32
        - name: user
          in: body
          description: ユーザ情報
          schema:
            $ref: "#/definitions/UserRequest"
      responses:
        200:
          description: 商品一覧の配列
          schema:
            $ref: '#/definitions/User'
        default:
          description: エラー
          schema:
            $ref: '#/definitions/Error'
    delete:
      summary: ユーザ削除
      parameters:
        - name: id
          in: path
          description: ユーザID
          required: true
          type: integer
          format: int32
      responses:
        200:
          description: Deleted
          schema:
            $ref: "#/definitions/Success"
### JSONモデルの定義
definitions:
  User:
    type: object
    properties:
      id:
        type: integer
        format: int32
      name:
        type: string
      email:
        type: string
        format: email
    required:
      - id
      - name
      - email
  UserRequest:
    type: object
    properties:
      name:
        type: string
      email:
        type: string
        format: email
    required:
      - name
      - email
  Success:
    type: object
    properties:
      code:
        type: integer
        format: int32
      message:
        type: string
      fields:
        type: string
  Error:
    type: object
    properties:
      code:
        type: integer
        format: int32
      message:
        type: string
      fields:
        type: string

大まかには「メタ情報」「エンドポイント定義」「モデル定義」に別れます。 メタ情報はAPIサーバのホスト名やポート番号、リクエスト可能なHTTPスキーマ(https)、使用するフォーマット(application/json)、必要な認証情報などを記載します。

paths はエンドポイントのパス( /products/{id} )、HTTPメソッド(get)の順に階層を記述し、その下層にAPIの説明文(summary, description)やリクエストパラメータ(parameters)、HTTPステータス別のレスポンスパラメータ(responses)を記述します。またAPIエンドポイントが多いときに利便性を向上するため、タグ(tags)などを指定できます。

parametersin

  • エンドポイントのパス(path)
  • クエリ文字列(query)
  • ヘッダ文字列(header)
  • フォーム形式(formData)
  • リクエストボディ(body)

が指定でき、例えば JSON形式のリクエストボディの場合は、

name: user
in: body
description: user to add to the system
required: true
schema:
  $ref: '#/definitions/UserRequest'

のように指定します。ここで $ref: '#/definitions/UserRequest' となっている部分はJSONで記述する UserRequest モデルの定義箇所を指定していて、paths の次の definitions セクションで記述します。

definitions:
  UserRequest:
    type: object
    properties:
      name:
        type: string
        description: 名前
      email:
        type: string
        format: email
        description: メールアドレス
    required:
      - name
      - email

nameemail の属性名と形式を指定し、どちらも必須です。

サーバ実装の生成

APIドキュメントが出来上がったら、 Generate Server から Node.js を選び、zipファイルをダウンロードします。これでひとまずのAPIサーバが手に入ります。

f:id:mather314:20161031194319p:plain

解凍すると以下のようなファイル構成になっています。

nodejs-server-server
├── LICENSE
├── README.md
├── api
│   └── swagger.yaml
├── controllers
│   ├── Default.js
│   └── DefaultService.js
├── index.js
└── package.json

api/swagger.yaml は先程記述したYAML定義そのもので、サーバ実装は controllers 以下に各エンドポイントの定義が書いてあり、 index.js がサービスを起動させます。

最初に実行するためには Node.js のパッケージを幾つかインストールする必要がありますので、このフォルダで以下のコマンドを実行し、 package.json に書かれているパッケージ一覧をインストールします。

$ npm install

インストールが完了すると、次のコマンドで起動できます。

$ node index.js
Your server is listening on port 8080 (http://localhost:8080)
Swagger-ui is available on http://localhost:8080/docs

http://localhost:8080/docs にアクセスすると、Swagger-UIというHTMLドキュメント兼クライアントが表示できます。

f:id:mather314:20161031195859p:plain

試しに GET /v1/user/1 を実行するにはこのようにします。

f:id:mather314:20161031200319p:plain

しかしこれは上手く行きませんでした。なぜならYAMLの定義にサーバのホスト名が sample.example.com と書いてあったため、リクエスト先に実際にサーバがないためです。 ローカルで試したい場合は api/swagger.yaml を少しだけ修正して http://localhost:8080 へのリクエストにしましょう。

- host: "sample.example.com"
+ host: "localhost:8080"
  basePath: "/v1"
  schemas:
- - "https"
+ - "http"

編集した後、ブラウザをリロードするとリクエスト先が変更されますので、再度リクエストをしてみると次のようにレスポンスが返ってきます。

f:id:mather314:20161031201255p:plain

このレスポンスは controllers/DefaultService.jsuserIdGET という関数によって以下のように定義されています。

exports.userIdGET = function(args, res, next) {
  /**
   * parameters expected in the args:
   * id (Integer)
   **/
  var examples = {};
  examples['application/json'] = {
    "name" : "aeiou",
    "id" : 123,
    "email" : "aeiou"
  };
  if(Object.keys(examples).length > 0) {
    res.setHeader('Content-Type', 'application/json');
    res.end(JSON.stringify(examples[Object.keys(examples)[0]] || {}, null, 2));
  } else {
    res.end();
  }
}

この値を直接変更したり、条件分岐を記述することで様々なレスポンスを返すようになります。 また、JSONボディによるPOSTリクエストでは数値のパラメータに文字列を記述するなどのデータフォーマット違反を検知するなど、API定義に記述された内容を満たしているかswagger-toolsモジュールが自動的にチェックしてくれます。

次回はAPI定義のパラメータに記述できるバリデーションや Node.js モックサーバをより使いやすくする方法に関して説明しようと思います。

*1:動作しないドキュメントはメンテナンスを怠ると陳腐化し弊害を生むことも多々あります。

*2:http://www.publickey1.jp/blog/15/open_api_initiative.html