Python Flask と BluePrint で REST API を作成する方法

2020年7月12日Flask

Python Flask を利用して REST API を作成する方法についてまとめました。

記事の内容としては以下の内容について解説してます。

記事の内容

  • Flask の BluePrint を利用して url のパス毎にファイルを分割する方法
  • CORS で別オリジン(ドメイン)からのアクセスに対応する方法
  • GET、PUT、POST、DELETEメソッドの実装方法
  • よく発生するエラーの対処方法
  • REST APIの呼び出し側サンプル

単純な REST API のサンプル

まずは、単純な REST API であれば、Flask で簡単に作成できることを体験するためにサンプルを用意しました。

サンプルの内容は「http://localhost:5000/」でアクセスした場合にJSON形式のメッセージを応答します。

from flask import Flask, jsonify

app = Flask(__name__)

@app.route('/')
def index():
    res = {'message': 'REST API Test'}

    return jsonify(res), 200

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

ブラウザで「http://localhost:5000/」にアクセスすると「{“message":"REST API Test"}」というメッセージが表示されるはずです。

Flask の BluePrint を利用して url のパス毎にファイルを分割する方法

例えば、クライアントからのリクエストが次のような場合に、3つのグループに分けて開発するとします。

  • http://localhost:5000/
  • http://localhost:5000/users
  • http://localhost:5000/users/<userid>
  • http://localhost:5000/products

1つ目はルート「/」でアクセスされた場合の処理

2つ目は/users で始まる url でアクセスされた場合の処理

3つ目は/products で始まる url でアクセスされた場合の処理

 

このような場合は、Flask の BluePrint を用いるとパス毎にファイルを分割することができます。

ファイルの構成

- main.py
  - route(フォルダ)
    - root.py
    - users.py
    - products.py

 

main.py

main.py は root.py、users.py、products.py を登録するのみです。

from flask import Flask

app = Flask(__name__)

app.register_blueprint(root)
app.register_blueprint(users)
app.register_blueprint(products)

 

root.py

root.py は「/」でアクセスされた場合の処理です。

from flask import Blueprint, jsonify

app = Blueprint('root', __name__, url_prefix='/')

@app.route('/', methods=['GET'])
def root():

    res = {
        'message': 'rootです'
    }

    return jsonify(res), 200

 

users.py

users.py は「/users」でアクセスされた場合の処理です。

from flask import Blueprint, request, jsonify

app = Blueprint('users', __name__, url_prefix='/users')

# データベースの代わり
usersData = {
    'users': [
        {
            'userId': 'UID0001',
            'userName': 'Admin',
        },
        {
            'userId': 'UID0002',
            'userName': 'test-userA',
        },
        {
            'userId': 'UID0003',
            'userName': 'test-userB',
        }
    ]
}


@app.route('/', methods=['GET'])
def getUserList():

    res = usersData

    return jsonify(res), 200


@app.route('/<userid>', methods=['GET'])
def getUserFromUserId(userid):

    users = [item for item in usersData.get('users') if item['userId'] == userid]

    res = {
        'users': users
    }

    return jsonify(res), 200

 

products.py

products.py は「/products」でアクセスされた場合の処理です。

from flask import Blueprint, request, jsonify

app = Blueprint('products', __name__, url_prefix='/products')

@app.route('/', methods=['GET'])
def getProductList():

    res = {
        'products': [
            {'id': 'S001', 'name': '商品A'},
            {'id': 'S002', 'name': '商品B'},
            {'id': 'S003', 'name': '商品C'}
        ]
    }

    return jsonify(res), 200

GET/POST/PUT/DELETE の各メソッドを実装してみる

users.py にユーザテーブルに対してデータの抽出・更新・削除を行う処理を実装してみます。

コードをシンプルにするためにデータベースではなく辞書型のデータを利用して説明します。

from flask import Blueprint, request, jsonify

app = Blueprint('users', __name__, url_prefix='/users')

# データベースの代わり
usersData = {
    'users': [
        {
            'userId': 'UID0001',
            'userName': 'Admin',
        },
        {
            'userId': 'UID0002',
            'userName': 'test-userA',
        },
        {
            'userId': 'UID0003',
            'userName': 'test-userB',
        }
    ]
}

# [http://localhost:5000/users/]にGETメソッドでリクエストされた場合
# 全ユーザの情報をレスポンスする
@app.route('/', methods=['GET'])
def getUserList():

    res = usersData

    return jsonify(res), 200


# [http://localhost:5000/users/<ユーザID>]にGETメソッドでリクエストされた場合
# 指定されたユーザIDの情報をレスポンスする
@app.route('/<userid>', methods=['GET'])
def getUserFromUserId(userid):

    users = [item for item in usersData.get('users') if item['userId'] == userid]
    
    res = {
        'users': users
    }

    return jsonify(res), 200


# [http://localhost:5000/users/]にPOSTメソッドでリクエストされた場合
# リクエスト情報に設定されたユーザ情報を追加して全ユーザの情報をレスポンスする
@app.route('/', methods=['POST'])
def addUser():

    req = request.json

    usersData.get('users').append(req)

    res = usersData

    return jsonify(res), 200


# [http://localhost:5000/users/<ユーザID>]にPUTメソッドでリクエストされた場合
# 指定されたユーザIDのデータをリクエスト情報に設定されたユーザ情報で更新して全ユーザの情報をレスポンスする
@app.route('/<userid>', methods=['PUT'])
def setUserFromUserId(userid):

    req = request.json

    users = []
    
    for item in usersData.get('users'):
        if item['userId'] == userid:
            users.append(req)
        else:
            users.append(item)
    
    usersData['users'] = users

    res = usersData

    return jsonify(res), 200


# [http://localhost:5000/users/<ユーザID>]にDELETEメソッドでリクエストされた場合
# 指定されたユーザIDのデータを削除し全ユーザの情報をレスポンスする
@app.route('/<userid>', methods=['DELETE'])
def deleteUserFromUserId(userid):

    users = [item for item in usersData.get('users') if item['userId'] != userid]

    usersData['users'] = users

    res = usersData

    return jsonify(res), 200

CORS を利用して別ORIGIN からのアクセスに対応する

ここまで作成してきたコードでもブラウザから GET メソッドを呼び出した場は正常にレスポンスされますが、ローカルの HTML ファイルや React、Vue、Angular などで作成したアプリからアクセスするとエラーになります。

解決方法を2パターンご紹介します。

別ORIGIN 対策1:flask_cors を利用する

from flask import Flask
from flask_cors import CORS

app = Flask(__name__)

CORS(app)

CORS(app)とした場合は、全てのリクエストに対して、別 ORIGIN からアクセスできるようになります。

 

/users 以下のリクエストのみ CORS を適用したい場合は、第2引数を指定することで実現できます。

CORS(app, resources={r"/users/*": {"origins": "*"}})

この場合、別 ORIGIN から/users にリクエストした場合は正常に応答されますが、/products にリクエストした場合はエラーとなります。

別ORIGIN 対策2:@app.after_request を利用する

効果は CORS と同じですが次のような記載方法でも実現できます。

from flask import Flask
from flask_cors import CORS

app = Flask(__name__)

@app.after_request
def after_request(res):
  res.headers.add('Access-Control-Allow-Origin', '*')
  res.headers.add('Access-Control-Allow-Headers', 'Content-Type')
  res.headers.add('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS')
  return res

よくあるエラーとその対応方法

レスポンスの日本語(全角文字)が文字化けする

事象

APIサーバ側で次のような日本語を含むレスポンスを返すと文字化けします。

  {
    'products': [
      {'id': 'S001', 'name': '商品A'},
      {'id': 'S002', 'name': '商品B'},
      {'id': 'S003', 'name': '商品C'}
    ]
  }

 

原因

Flaskの’JSON_AS_ASCII’の初期値がTrueになっているため全角文字などが文字化けする

 

対策

Flaskのアプリケーションに’JSON_AS_ASCII’をFalseで設定する

  app = Flask(__name__)
  app.config['JSON_AS_ASCII'] = False

別 ORIGIN からアクセスできない

事象

別ORIGINからAPIを呼び出した場合に、次のようなエラーメッセージが表示される。

Access to fetch at 'http://localhost:5000/users/' from origin 'null' has been blocked by CORS policy:
Response to preflight request doesn't pass access control check: 
No 'Access-Control-Allow-Origin' header is present on the requested resource. 
If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

 

原因

「CORS の設定がされていない」もしくは「CORS の設定が間違っている」可能性が高いです。

 

対策

「CORS を利用して別 ORIGIN からのアクセスに対応する」を参考にしてください。

リクエストで Headers を設定した場合にエラーになる

事象

リクエスト側で Headers を指定している場合に、次のようなエラーが発生する。

Access to fetch at 'http://localhost:5000/users/' from origin 'null' has been blocked by CORS policy:
Request header field content-type is not allowed by Access-Control-Allow-Headers in preflight response.

 

原因

APIサーバ側でHeadersを受け取らない設定なっている

 

対策

API サーバ側で Header を受け取れるようにする必要があります。

どちらかの方法で解決できます。

  from flask import Flask
  from flask_cors import CORS

  app = Flask(__name__)

  CORS(app)
  from flask import Flask

  app = Flask(__name__)

  @app.after_request
  def after_request(res):
    res.headers.add('Access-Control-Allow-Headers', 'Content-Type')
    return res

PUTやDELETEメソッドがエラーになる

事象

PUT、DELETEメソッドを呼び出した場合に、次のようなエラーになる

Access to fetch at 'http://localhost:5000/users/UID0002' from origin 'null' has been blocked by CORS policy:
Method DELETE is not allowed by Access-Control-Allow-Methods in preflight response.

 

原因

'Access-Control-Allow-Methods’でにPUTやDELETEメソッドを許可していない

※GETやPOSTメソッドは許可設定なしでもエラーとならない

 

対策

利用するメソッドを’Access-Control-Allow-Methods’に利用するメソッドを追加する

res.headers.add('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS')

REST APIの呼び出し側サンプル

静的なHTMLファイルとJavaScriptのfetchで作成したREST APIの呼び出しサンプルです。

別ORIGINからのリクエストは/usersのみ許可する設定にしています。

<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css"
    integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">
  <title>Flask REST API Test Page</title>
</head>

<body>
  <div class="container mt-5">
    <div class="row">
      <div class="col">

        <input type="button" class="btn btn-primary mb-3" value="[GET] /"
          onclick="callWepApi('http://localhost:5000/', 'GET');">

        <input type="button" class="btn btn-primary mb-3" value="[GET] /users/"
          onclick="callWepApi('http://localhost:5000/users/', 'GET');">

        <input type="button" class="btn btn-primary mb-3" value="[GET] /users/UID0001"
          onclick="callWepApi('http://localhost:5000/users/UID0001', 'GET');">

        <input type="button" class="btn btn-success mb-3" value="[POST] /users/"
          onclick="callWepApi('http://localhost:5000/users/', 'POST', postData);">

        <input type="button" class="btn btn-warning mb-3" value="[PUT] /users/UID0003"
          onclick="callWepApi('http://localhost:5000/users/UID0003', 'PUT', putData);">

        <input type="button" class="btn btn-danger mb-3" value="[DELETE] /users/UID0002"
          onclick="callWepApi('http://localhost:5000/users/UID0002', 'DELETE');">

        <input type="button" class="btn btn-primary mb-3" value="[GET] /products/"
          onclick="callWepApi('http://localhost:5000/products/', 'GET');">

        <input type="button" class="btn btn-dark mb-3" value="[GET] /aaa => ERROR"
          onclick="callWepApi('http://localhost:5000/aaa', 'GET');">

      </div>
    </div>
    <div class="row mt-5">
      <div class="col">
        <div class="form-group">
          <label for="res" class="border text-center w-100 p-2 mb-0 bg-secondary text-white">Response JSON</label>
          <pre id="res" class="border p-2"></pre>
        </div>
      </div>
    </div>
  </div>

  <script>

    // 追加用のPOSTデータ
    const postData = {
      'userId': 'UID0004',
      'userName': 'test-userC'
    }

    // 更新用のPUTデータ
    const putData = {
      'userId': 'UID0003',
      'userName': 'update-user'
    }


    const callWepApi = (url, method, data = null) => {
      fetch(url, {
        method: method,
        headers: {
          'Content-Type': 'application/json; charset=utf-8',
        },
        body: data && JSON.stringify(data),
      })
        .then(Response => Response.json())
        .then(data => {
          // JSON文字列が見やすいように整形
          document.getElementById('res').innerText = JSON.stringify(data, null, 2);
        })
        .catch(error => {
          document.getElementById('res').innerText = error;
        })
    }

  </script>

</body>

</html>

GETメソッド「/users/」

「[GET] /users/」ボタンで「http://localhost:5000/users/」へリクエストした場合

{
  "users": [
    {
      "userId": "UID0001",
      "userName": "Admin"
    },
    {
      "userId": "UID0002",
      "userName": "test-userA"
    },
    {
      "userId": "UID0003",
      "userName": "test-userB"
    }
  ]
}

Flask側の初期設定のusersDataの全件を取得できました。

GETメソッド「/users/UID0001」

「[GET] /users/UID0001」ボタンで「http://localhost:5000/users/UID0001」へリクエストした場合

{
  "users": [
    {
      "userId": "UID0001",
      "userName": "Admin"
    }
  ]
}

useridがUID0001のデータのみが取得できました。

POSTメソッド「/users/」

「[POST] /users/」ボタンで「http://localhost:5000/users/」へリクエストした場合

{
  "users": [
    {
      "userId": "UID0001",
      "userName": "Admin"
    },
    {
      "userId": "UID0002",
      "userName": "test-userA"
    },
    {
      "userId": "UID0003",
      "userName": "test-userB"
    },
    {
      "userId": "UID0004",
      "userName": "test-userC"
    }
  ]
}

送信したuseridがUID0004のデータが追加されています。

PUTメソッド「/users/UID0003」

「[PUT] /users/UID0003」ボタンで「http://localhost:5000/users/UID0003」へリクエストした場合

{
  "users": [
    {
      "userId": "UID0001",
      "userName": "Admin"
    },
    {
      "userId": "UID0002",
      "userName": "test-userA"
    },
    {
      "userId": "UID0003",
      "userName": "update-user"
    },
    {
      "userId": "UID0004",
      "userName": "test-userC"
    }
  ]
}

送信したuseridがUID0003のデータのユーザ名が「test-userB」から「update-user」に更新されました。

DELETEメソッド「/users/UID0002」

「[DELETE] /users/UID0002」ボタンで「http://localhost:5000/users/UID0002」へリクエストした場合

{
  "users": [
    {
      "userId": "UID0001",
      "userName": "Admin"
    },
    {
      "userId": "UID0003",
      "userName": "update-user"
    },
    {
      "userId": "UID0004",
      "userName": "test-userC"
    }
  ]
}

useridがUID0002のデータが削除されました。

GETメソッド「/products/」

「[GET] /products/」ボタンで「http://localhost:5000/products/」へリクエストした場合

TypeError: Failed to fetch

/users以外のアクセスを許可していないので、エラーとなっています。

GETメソッド「/aaa」

「[GET] /aaa」ボタンで「http://localhost:5000/aaa」へリクエストした場合

TypeError: Failed to fetch

存在しないパスの場合はエラーとなります。

ログで確認するとステータスコードは404となっていますので、想定した動作です。

Flask

Posted by snow