Flask-RESTPlusでREST APIとSwaggerドキュメントを統合

# はじめに
この記事は [Muroran Institute of Technology Advent Calendar 2017](https://adventar.org/calendars/2272) 25日目の記事です。

Flaskを使ってREST APIを作りたいときに、同時にドキュメントとしてSwaggerも整備したいこと、あると思います。しかしドキュメントも書きつつAPIも書くとどっちかを更新したときにどっちかを更新し忘れるなどして、だんだん面倒になってきてしまいがちです。

そこで今回は簡単なToDo管理APIを例に、`Flask-RESTPlus`を使ってAPI定義とSwaggerによるドキュメント生成を一気に行ってしまう方法をご紹介します。

# Python環境の準備
とりあえずPython環境を準備しておきましょう。PyenvとVirtualenvを使ってさくさくっと作っていきます。仮想環境名は`restplus`にしてありますが別に何でもかまいません。今回は最新版であるPython3.6.4を用います。

```bash
$ mkdir restplus
$ cd restplus
$ pyenv virtualenv 3.6.4 restplus
$ pyenv local restplus
```

# ファイルの準備
ではファイルやディレクトリを準備していきます。以下のような構造で準備してください。それぞれのファイルが何に使われるかは後述します。

```
restplus/
├── rest_api/
│   ├── apis/
│   │   └── __init__.py
│   ├── models/
│   │   └── __init__.py
│   ├── __init__.py
│   ├── app.py
│   └── settings.py
├── Dockerfile
├── docker-compose.yml
└── requirements.txt
```

# Pythonライブラリの導入
## requirements.txtの編集
requirements.txtには今回利用するPythonライブラリを列挙していきます。以下のように書いておきましょう。Flaskを使うのでORMにはSQL Alchemyを採用しました。また、データベースはMySQLを利用することにします。それに伴って`flask-sqlalchemy`と`pymysql`をインストールしておきます。

```:requirements.txt
flask
flask-restplus
flask-sqlalchemy
pymysql
```

## ライブラリのインストール
編集できたら以下のコマンドでインストールしてしまいます。ぶっちゃけるとDockerで動かすので入れなくても良いといえばいいのですが、IDEなどで補完を効かせるために入れておきましょう。

```bash
$ pip install -r requirements.txt
```

# コア部分の作成
ここまでできたらいよいよコーディングに入ります。Flaskを使ったWebアプリをいつも通りな感じで構築していきます。

## APIエンドポイントの準備
`apis/__init__.py`にAPIエンドポイントを登録する準備をします。Flask RESTPlusで用意された`API`オブジェクトを初期化しておきましょう。

```python3:apis/__init__.py
from flask_restplus import Api

# API情報を指定して初期化
api = Api(
    title='Test API',
    version='1.0',
    description='Swaggerを統合したREST APIのサンプル'
)
```

## ORMの準備
`models/__init__.py`ではSQL Alchemyの初期化を行います。このファイルにはDBのスキーマを定義していきます。

```python3:models/__init__.py
from flask_sqlalchemy import SQLAlchemy

# SQLAlchemyの初期化
db = SQLAlchemy()
```

## アプリの諸設定
`settings.py`にはアプリの設定を記述していきます。特にSQL接続情報などの記載は必須です。

```python3:settings.py
from os import environ

# デバッグモードを有効化
DEBUG = True

# Swaggerのデフォルト表示形式をListにする
SWAGGER_UI_DOC_EXPANSION = 'list'
# Validationの有効化
RESTPLUS_VALIDATE = True

# SQL接続情報
# コンテナ側に環境変数として渡すためこの形式で受け取る
SQLALCHEMY_DATABASE_URI = environ['MYSQL_URL']
SQLALCHEMY_TRACK_MODIFICATIONS = True
```

## Flaskアプリの定義
ここまで準備できたらFlaskアプリを定義していきます。`app`変数にFlaskオブジェクトを入れて、それを引数で取り回しながらAPIエンドポイントやSQL Alchemyとの連携を行います。最後に`run`メソッドを実行すればOKです。このとき、Dockerの中で実行されるため`host`の指定を忘れないようにしましょう。

```python3:app.py
from flask import Flask

from rest_api import settings
from rest_api.apis import api
from rest_api.models import db

# Flask本体
app = Flask(__name__)


def configure_app(flask_app: Flask) -> None:
    # DB接続先情報やSwaggerの表示形式を指定
    flask_app.config['SQLALCHEMY_DATABASE_URI'] = settings.SQLALCHEMY_DATABASE_URI
    flask_app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = settings.SQLALCHEMY_TRACK_MODIFICATIONS
    flask_app.config['SWAGGER_UI_DOC_EXPANSION'] = settings.SWAGGER_UI_DOC_EXPANSION
    flask_app.config['RESTPLUS_VALIDATE'] = settings.RESTPLUS_VALIDATE


def initialize_app(flask_app: Flask) -> None:
    # FlaskへAPIやDB情報を登録
    configure_app(flask_app)
    api.init_app(flask_app)
    db.init_app(flask_app)
    db.create_all(app=flask_app)


def main() -> None:
    # Flaskを初期化して実行
    initialize_app(app)
    app.run(host='0.0.0.0', debug=settings.DEBUG)


if __name__ == '__main__':
    main()
```

# Dockerの準備
## Dockerfile
アプリをDockerに載せるためにDockerfileを書きます。MySQLの起動を待つためにDockerizeも一緒に仕込んでおきます。

```dockerfile
FROM python:alpine3.6

WORKDIR /usr/src/app

ENV DOCKERIZE_VERSION v0.6.0

COPY requirements.txt ./
RUN pip install -U --no-cache-dir -r requirements.txt \
 && apk add --no-cache openssl \
 && wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-alpine-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
 && tar -C /usr/local/bin -xzvf dockerize-alpine-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
 && rm dockerize-alpine-linux-amd64-$DOCKERIZE_VERSION.tar.gz

COPY . .
CMD [ "python", "rest_api/app.py" ]
```

## docker-compose.yml
DockerfileだけではMySQLとの統合が難しいのでdocker-compose.ymlも書いておきましょう。アプリ内部でのimportを使うため、該当ディレクトリを`PYTHONPATH`環境変数に渡してあげるのを忘れないようにしてください。また、MySQLの接続情報も渡してあげましょう。

```yaml
version: '3'

services:
  restapi:
    build: .
    ports:
      - "5000:5000"
    environment:
      MYSQL_URL: 'mysql+pymysql://test:user_pass@mysql/test'
      PYTHONPATH: '/usr/src/app'
    links:
      - mysql
    entrypoint:
      - dockerize
      - -timeout
      - 60s
      - -wait
      - tcp://mysql:3306
    command: python /usr/src/app/rest_api/app.py

  mysql:
    image: mariadb:latest
    environment:
      MYSQL_ROOT_PASSWORD: 'root_pass'
      MYSQL_DATABASE: 'test'
      MYSQL_USER: 'test'
      MYSQL_PASSWORD: 'user_pass'
      command: mysqld --character-set-server=utf8 --collation-server=utf8_unicode_ci
```

## 起動テスト
ここまでできたら起動のテストを行います。docker-composeでさくさくっとやってしまいましょう。

```bash
$ docker-compose build
$ docker-compose up -d
```

起動し終わったら、[http://localhost:5000/](http://localhost:5000/) にアクセスすると以下のような画面が表示されます。ここにだんだんとAPIの仕様書ができあがっていきます。

![Flask_01.PNG](https://qiita-image-store.s3.amazonaws.com/0/53168/a3dca6a0-a850-77d9-47bd-18c7339d52d1.png)

# DBスキーマの定義
続いてデータベースのスキーマを定義していきます。`models/__init__.py`につらつらと書いていきます。ToDoに使うスキーマを簡単に定義します。`models/__init__.py`に以下の記述を追記します。`db`変数に格納したSQLAlchemyオブジェクトを流用して、カラムやその型を定義します。このとき、`primary_key`に設定した`Integer`のカラムはオートインクリメントが適用されます。

```python3:models/__init__.py
from flask_sqlalchemy import SQLAlchemy

# SQLAlchemyの初期化
db = SQLAlchemy()


# Userスキーマの定義
class User(db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer, nullable=False, primary_key=True)
    name = db.Column(db.Text, nullable=False)
    email = db.Column(db.Text, nullable=False)


# ToDoスキーマの定義
class ToDo(db.Model):
    __tablename__ = 'todo'
    id = db.Column(db.Integer, nullable=False, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
    title = db.Column(db.Text, nullable=False)
    description = db.Column(db.Text, nullable=False)
```

# APIエンドポイントの定義
## 定義ファイルの作成
データベースの準備ができたらAPIエンドポイントを設計しましょう。`apis/`ディレクトリに`todo.py`と`user.py`を新規作成し、そこに定義を書いていきます。

```bash
$ touch apis/todo.py apis/user.py
```

## NamespaceとJSONモデルの定義
異なるAPIエンドポイントを区別するためにNamespaceを作成します。さらに、返したいJSONのモデルも定義しましょう。使ってもいないのに`Resource`をimportしていますが、この後すぐ使うのでここでimportしてしまいます。

Namespaceオブジェクトを生成する際の第1引数にはエンドポイントのプレフィックスを指定します。例えばtodo.pyでは`todo`と指定していますが、`/todo`以下でこれから定義するエンドポイントが呼べるようになります。作成したNamespaceは忘れずにAPIに登録してください。

JSONモデルの定義はDBのカラム名と同様の文字列をキーにした辞書で行います。各要素について必須かどうかや詳しい説明、例となる値を指定することができます。これらの情報は全てSwaggerドキュメントに反映されます。

```python3:apis/todo.py
from flask_restplus import Namespace, fields, Resource

# Namespaceの初期化
todo_namespace = Namespace('todo', description='ToDoのエンドポイント')

# JSONモデルの定義
todo = todo_namespace.model('ToDo', {
    'user_id': fields.Integer(
        required=True,
        description='ToDoを登録したユーザーID',
        example='0'
    ),
    'id': fields.Integer(
        required=True,
        description='ToDoのID',
        example='1'
    ),
    'title': fields.String(
        required=True,
        description='ToDoタイトル',
        example='起きる'
    ),
    'description': fields.String(
        required=True,
        description='ToDoの詳細',
        example='朝7時に起きたい'
    )
})
```

```python3:apis/user.py
from flask_restplus import Namespace, fields, Resource

# Namespaceの初期化
user_namespace = Namespace('users', description='ユーザー関連のエンドポイント')

# JSONモデルの定義
user = user_namespace.model('User', {
    'id': fields.Integer(
        required=True,
        description='',
        example='0'
    ),
    'name': fields.String(
        required=True,
        description='',
        example='Aruneko'
    ),
    'email': fields.String(
        required=True,
        description='',
        example='aruneko@example.com'
    )
})

```

## エンドポイントの追加
NemespaceとJSONモデルが定義できたらいよいよエンドポイントを追加します。やり方は簡単で、`Resource`クラスを継承したクラスを作ってHTTPメソッドと同名のメソッドを作成し、ルーティングに必要なアノテーションを付加するだけです。これらを踏まえて、apis以下のファイルの最終形は以下のようになります。

各メソッドの先頭にダブルクォートを6つ使った複数行コメントを差し込むことで、API説明をSwaggerに反映させることができます。1行目が簡易的な説明で、2行目以降はMarkdown記法を使った詳細な説明を記入できます。

`marshal_with`や`marshal_list_with`では返ってくるJSONの形式を指定できます。前者は単体を、後者はリスト化したものを返します。先ほど定義したJSONモデルを引数にとってあげましょう。`expect`はその逆で、送って欲しいJSONのモデルを指定します。

```python3:apis/todo.py
from flask_restplus import Namespace, fields, Resource

from rest_api.apis import api
from rest_api.models import ToDo, db

# Namespaceの初期化と登録
todo_namespace = Namespace('todo', description='ToDoのエンドポイント')

# JSONモデルの定義
todo = todo_namespace.model('ToDo', {
    'user_id': fields.Integer(
        required=True,
        description='ToDoを登録したユーザーID',
        example='0'
    ),
    'id': fields.Integer(
        required=True,
        description='ToDoのID',
        example='1'
    ),
    'title': fields.String(
        required=True,
        description='ToDoタイトル',
        example='起きる'
    ),
    'description': fields.String(
        required=True,
        description='ToDoの詳細',
        example='朝7時に起きたい'
    )
})


@todo_namespace.route('/')
class ToDoList(Resource):
    # todoモデルを利用して結果をパースしてリストで返す
    @todo_namespace.marshal_list_with(todo)
    def get(self):
        """
        一覧取得
        """
        return ToDo.query.all()

    @todo_namespace.marshal_with(todo)
    @todo_namespace.expect(todo, validate=True)
    def post(self):
        """
        ToDo登録
        """
        # ちょっとやっかいなので実装はまた今度
        pass


@todo_namespace.route('/<int:todo_id>')
class ToDoController(Resource):
    # todoモデルを利用して結果をパースして単体で返す
    @todo_namespace.marshal_with(todo)
    def get(self, todo_id):
        """
        ToDo詳細
        """
        # ただし1個も見つからなかったら404を返す
        return ToDo.query.filter(ToDo.id == todo_id).first_or_404()

    def delete(self, todo_id):
        """
        ToDo削除
        """
        # 見つからなかったときの処理してないけど許して
        target_todo = ToDo.query.filter(ToDo.id == todo_id).first()
        db.session.delete(target_todo)
        return {'message': 'Success'}, 200
```

```python3:apis/user.py
from flask_restplus import Namespace, fields, Resource

from rest_api.apis import api

# Namespaceの初期化と登録
from rest_api.apis.todo import todo
from rest_api.models import User, db, ToDo

user_namespace = Namespace('users', description='ユーザー関連のエンドポイント')

# JSONモデルの定義
user = user_namespace.model('User', {
    'id': fields.Integer(
        required=True,
        description='',
        example='0'
    ),
    'name': fields.String(
        required=True,
        description='',
        example='Aruneko'
    ),
    'email': fields.String(
        required=True,
        description='',
        example='aruneko@example.com'
    )
})


@user_namespace.route('/')
class UserList(Resource):
    @user_namespace.marshal_list_with(user)
    def get(self):
        """
        ユーザー一覧
        """
        return User.query.all()

    @user_namespace.marshal_with(user)
    @user_namespace.expect(user)
    def post(self):
        """
        ユーザー登録
        """
        # ちょっとやっかいなので実装はまた今度
        pass


@user_namespace.route('/<int:user_id>')
class UserController(Resource):
    @user_namespace.marshal_with(user)
    def get(self, user_id):
        """
        ユーザー詳細
        """
        return User.query.filter(User.id == user_id).first_or_404()

    def delete(self, user_id):
        """
        ユーザー削除
        """
        target_user = User.query.filter(User.id == user_id).first()
        db.session.delete(target_user)
        return {'message': 'Success'}, 200


@user_namespace.route('/<int:user_id>/todo')
class ToDoByUser(Resource):
    @user_namespace.marshal_list_with(todo)
    def get(self, user_id):
        """
        ユーザーごとのToDo取得
        """
        return ToDo.query.filter(ToDo.user_id == user_id).all()
```

## エンドポイントの登録
エンドポイントを作成し終わったら登録を行います。`apis/__init__.py`に記入しましょう。`add_namespace`メソッドを利用してください。

```python3:apis/__init__.py
from flask_restplus import Api

from rest_api.apis.todo import todo_namespace
from rest_api.apis.user import user_namespace

# API情報を指定して初期化
api = Api(
    title='Test API',
    version='1.0',
    description='Swaggerを統合したREST APIのサンプル'
)

api.add_namespace(todo_namespace)
api.add_namespace(user_namespace)

```

## テスト起動
再びdocker-composeを使ってテスト起動してみます。

```bash
$ docker-compose build
$ docker-compose up -d
```

[http://localhost:5000/](http://localhost:5000/) を確認してください。以下のように、APIドキュメントが表示されるはずです。

![完成したAPIドキュメント](https://qiita-image-store.s3.amazonaws.com/0/53168/1e797b95-9f8d-f391-dc01-be394317ec99.png)

各項目をクリックすると、実際にAPIを試すことができるようになっています。また、先ほど指定したJSONモデルもしっかりと表示されていることがわかります。

![Flask_03.PNG](https://qiita-image-store.s3.amazonaws.com/0/53168/6822ea2b-f236-611a-7aed-0cf2aee0143b.png)

さらに、curlなどでアクセスすればきちんとレスポンスが返ってくることも確認できます。今回はデータを入れていないので空のJSONだけが返ってきますけれども。

```bash
$ curl http://localhost:5000/todo/ 
[]
```

# おわりに
ここまでFlask-RESTPlusを使ってSwaggerを統合したRESTfulなAPIを作成してきました。モデルの定義とドキュメント作成が同時に行えるので非常に便利です。

今回は紹介しませんでしたが、このほかにもリクエストのパースやエラーハンドリングなど、多彩な機能を取りそろえています。詳しいことは[公式ドキュメント](http://flask-restplus.readthedocs.io/en/stable/)も参考にしてみてください。

最後になりましたが、 [Muroran Institute of Technology Advent Calendar 2017](https://adventar.org/calendars/2272) にご参加いただいた皆さん、ありがとうございました！