Python Flask と BluePrint で REST API を作成する方法
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] /">
<input type="button" class="btn btn-primary mb-3" value="[GET] /users/">
<input type="button" class="btn btn-primary mb-3" value="[GET] /users/UID0001">
<input type="button" class="btn btn-success mb-3" value="[POST] /users/">
<input type="button" class="btn btn-warning mb-3" value="[PUT] /users/UID0003">
<input type="button" class="btn btn-danger mb-3" value="[DELETE] /users/UID0002">
<input type="button" class="btn btn-primary mb-3" value="[GET] /products/">
<input type="button" class="btn btn-dark mb-3" value="[GET] /aaa => ERROR">
</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となっていますので、想定した動作です。