Flask RESTful APIの基礎

2026-02-19

はじめに

Web開発においてRESTful APIは現代的なアプリケーション構築に不可欠な要素です。Flaskは軽量でありながら強力なWebフレームワークとして、API開発にも優れた柔軟性を提供します。本記事ではFlaskを使ったRESTful API開発の基礎を、初心者の方にもわかりやすく解説します。API設計の基本原則から実際の実装方法まで、段階的に学んでいきましょう。

APIの設計原則

RESTful APIを設計する際には、いくつかの重要な原則に従うことが推奨されます。これらの原則はAPIの一貫性、保守性、使いやすさを保証します。

まず、リソースベースの設計が基本となります。APIはデータやサービスをリソースとして扱い、それぞれのリソースに対して一意のURLを割り当てます。例えば、ユーザー管理システムであれば「users」というリソースを中心に設計します。

HTTPメソッドを適切に使用することも重要です。GETメソッドはリソースの取得、POSTメソッドは新規作成、PUTメソッドは更新、DELETEメソッドは削除というように、各HTTPメソッドには特定の意味があります。

ステートレス性の維持もRESTの重要な原則です。各リクエストはそれ自体で完結しており、サーバーはクライアントの状態を保持する必要がありません。これによりスケーラビリティと信頼性が向上します。

統一されたインターフェースを提供することも大切です。エンドポイントの命名規則、エラーハンドリング、レスポンス形式などに一貫性を持たせることで、API利用者が理解しやすくなります。

以下に、これらの原則を実践する簡単な例を示します。

from flask import Flask, jsonify, request

app = Flask(__name__)

# サンプルデータ
users = [
    {"id": 1, "name": "山田太郎", "email": "yamada@example.com"},
    {"id": 2, "name": "佐藤花子", "email": "sato@example.com"}
]

@app.route('/users', methods=['GET'])
def get_users():
    return jsonify(users)

@app.route('/users', methods=['POST'])
def create_user():
    new_user = request.get_json()
    users.append(new_user)
    return jsonify(new_user), 201

JSONの返却

現代のWeb APIでは、データ形式としてJSONが広く採用されています。JSONは軽量で人間が読みやすく、様々なプログラミング言語で簡単に処理できるという利点があります。

Flaskではjsonify関数を使用して簡単にJSONレスポンスを生成できます。この関数はPythonの辞書やリストをJSON形式に変換し、適切なContent-Typeヘッダーを設定します。

レスポンスに適切なHTTPステータスコードを設定することも重要です。成功時の200 OK、新規作成時の201 Created、リソース不存在時の404 Not Foundなど、状況に応じたステータスコードを使用します。

エラーハンドリングもAPIの重要な部分です。エラーが発生した場合には、一貫性のあるエラーレスポンスを返すようにします。エラーメッセージとともに、デバッグに役立つ情報を含めることが推奨されます。

from flask import Flask, jsonify, request

app = Flask(__name__)

products = [
    {"id": 1, "name": "ノートパソコン", "price": 120000},
    {"id": 2, "name": "ワイヤレスマウス", "price": 3500}
]

@app.route('/products', methods=['GET'])
def get_products():
    # 商品リストをJSONで返却
    return jsonify({
        "status": "success",
        "data": products,
        "count": len(products)
    })

@app.route('/products/', methods=['GET'])
def get_product(product_id):
    # 特定の商品を検索
    product = next((p for p in products if p['id'] == product_id), None)

    if product is None:
        # 商品が見つからない場合のエラーレスポンス
        return jsonify({
            "status": "error",
            "message": "商品が見つかりません",
            "code": 404
        }), 404

    # 商品情報をJSONで返却
    return jsonify({
        "status": "success",
        "data": product
    })

@app.errorhandler(404)
def not_found(error):
    # 404エラーのカスタムハンドリング
    return jsonify({
        "status": "error",
        "message": "リソースが見つかりません",
        "code": 404
    }), 404

基本的なAPIエンドポイント作成

実際に動作するRESTful APIを作成するには、いくつかの基本的なエンドポイントを実装する必要があります。ここでは書籍管理APIを例に、CRUD操作(Create, Read, Update, Delete)に対応するエンドポイントを作成します。

まず、Flaskアプリケーションの設定と必要なインポートを行います。requestオブジェクト用于リクエストデータの取得、jsonify用于レスポンスの生成に使用します。

データの永続化については、本来はデータベースを使用しますが、学習用としてメモリ上にデータを保持する方法から始めます。実際のプロジェクトではSQLAlchemyなどのORMを導入することが一般的です。

各エンドポイントでは、適切なHTTPメソッドを割り当て、リクエストパラメータの検証、ビジネスロジックの実行、適切なレスポンスの返却という流れを実装します。

from flask import Flask, jsonify, request

app = Flask(__name__)

# 簡易的なデータストア
books = [
    {"id": 1, "title": "Python入門", "author": "技術太郎", "year": 2020},
    {"id": 2, "title": "Flask Web開発", "author": "開発花子", "year": 2021}
]

@app.route('/books', methods=['GET'])
def get_books():
    # クエリパラメータの処理(オプション)
    author = request.args.get('author')

    if author:
        filtered_books = [b for b in books if b['author'] == author]
        return jsonify({
            "status": "success",
            "data": filtered_books,
            "count": len(filtered_books)
        })

    return jsonify({
        "status": "success",
        "data": books,
        "count": len(books)
    })

@app.route('/books/', methods=['GET'])
def get_book(book_id):
    book = next((b for b in books if b['id'] == book_id), None)

    if book is None:
        return jsonify({
            "status": "error",
            "message": f"ID {book_id}の書籍は見つかりません"
        }), 404

    return jsonify({
        "status": "success",
        "data": book
    })

@app.route('/books', methods=['POST'])
def create_book():
    data = request.get_json()

    # 簡単なバリデーション
    if not data or 'title' not in data or 'author' not in data:
        return jsonify({
            "status": "error",
            "message": "タイトルと著者は必須項目です"
        }), 400

    # 新しい書籍の作成
    new_id = max([b['id'] for b in books]) + 1 if books else 1
    new_book = {
        "id": new_id,
        "title": data['title'],
        "author": data['author'],
        "year": data.get('year', 2023)
    }

    books.append(new_book)

    return jsonify({
        "status": "success",
        "message": "書籍が正常に作成されました",
        "data": new_book
    }), 201

@app.route('/books/', methods=['PUT'])
def update_book(book_id):
    book = next((b for b in books if b['id'] == book_id), None)

    if book is None:
        return jsonify({
            "status": "error",
            "message": f"ID {book_id}の書籍は見つかりません"
        }), 404

    data = request.get_json()

    # 書籍情報の更新
    if 'title' in data:
        book['title'] = data['title']
    if 'author' in data:
        book['author'] = data['author']
    if 'year' in data:
        book['year'] = data['year']

    return jsonify({
        "status": "success",
        "message": "書籍が正常に更新されました",
        "data": book
    })

@app.route('/books/', methods=['DELETE'])
def delete_book(book_id):
    global books
    book = next((b for b in books if b['id'] == book_id), None)

    if book is None:
        return jsonify({
            "status": "error",
            "message": f"ID {book_id}の書籍は見つかりません"
        }), 404

    # 書籍の削除
    books = [b for b in books if b['id'] != book_id]

    return jsonify({
        "status": "success",
        "message": "書籍が正常に削除されました"
    }), 200

if __name__ == '__main__':
    app.run(debug=True)

このコード例では、書籍リソースに対する完全なCRUD操作を実装しています。GETリクエスト用于データの取得、POST用于新規作成、PUT用于更新、DELETE用于削除を行います。

APIをテストするには、以下のようなcurlコマンドを使用できます。

# すべての書籍を取得
curl http://localhost:5000/books

# 特定の書籍を取得
curl http://localhost:5000/books/1

# 新しい書籍を作成
curl -X POST -H "Content-Type: application/json" -d '{"title": "新しい本", "author": "著者名", "year": 2023}' http://localhost:5000/books

# 書籍を更新
curl -X PUT -H "Content-Type: application/json" -d '{"title": "更新されたタイトル"}' http://localhost:5000/books/1

# 書籍を削除
curl -X DELETE http://localhost:5000/books/1

まとめ

API設計の原則、JSON形式でのデータ返却、基本的なエンドポイントの実装方法を学び、実際に動作する書籍管理APIのサンプルを作成しました。

RESTful APIの開発では、一貫性のある設計、適切なHTTPメソッドの使用、明確なエラーハンドリングが重要です。Flaskのシンプルな構造は、これらの概念を理解し実装するのに最適な環境を提供します。

次のステップとして、データベース連携、認証・認可の実装、APIバージョニング、ドキュメント生成などの高度なトピックに進むことをお勧めします。Flask-RESTfulやFlask-RESTXなどの拡張ライブラリを使用すると、より構造化されたAPI開発が可能になります。

演習問題

初級問題

問題1: 基本的なエンドポイント作成

書籍情報を返す簡単なAPIを作成してください。/booksエンドポイントで以下の書籍データをJSON形式で返すFlaskアプリを作成しましょう。

書籍データ:

books = [
    {"id": 1, "title": "Python入門", "author": "山田太郎"},
    {"id": 2, "title": "Flask Web開発", "author": "佐藤花子"}
]

問題2: 特定の書籍取得

書籍IDを指定して特定の書籍情報を取得するエンドポイントを追加してください。/books/<int:book_id>エンドポイントで、該当する書籍があればその情報を、なければ404エラーを返すようにしましょう。

問題3: 簡単な書籍検索

著者名で書籍を検索できる機能を追加してください。/booksエンドポイントにクエリパラメータauthorを指定できるようにし、指定された著者の書籍のみを返すようにしましょう。

中級問題

問題4: 書籍の追加機能

新しい書籍を追加するPOSTエンドポイントを作成してください。リクエストボディからタイトルと著者名を受け取り、新しい書籍を追加する機能を実装しましょう。IDは自動採番されるようにします。

問題5: 書籍更新機能

既存の書籍情報を更新するPUTエンドポイントを作成してください。書籍IDを指定し、リクエストボディで更新するフィールドを受け取って更新する機能を実装しましょう。

問題6: 書籍削除機能

書籍を削除するDELETEエンドポイントを作成してください。指定されたIDの書籍を削除し、適切なレスポンスを返す機能を実装しましょう。

問題7: エラーハンドリング

存在しないエンドポイントへのアクセスや、不正なリクエストに対して適切なエラーレスポンスを返すエラーハンドラを実装してください。

問題8: レスポンスの統一

すべてのレスポンスを統一された形式で返すように改良してください。成功時とエラー時で一貫したJSON構造を持つようにします。

問題9: バリデーション機能

書籍追加時のバリデーションを強化してください。タイトルと著者名が空でないこと、タイトルが重複しないことをチェックする機能を追加しましょう。

上級問題

問題10: ページネーション機能

書籍一覧取得APIにページネーション機能を追加してください。pageper_pageパラメータを受け取り、指定されたページのデータのみを返すようにしましょう。

問題11: データ永続化

現在メモリ上にある書籍データをJSONファイルに保存・読み込みする機能を追加してください。アプリ起動時にファイルからデータを読み込み、変更時にファイルに保存するようにしましょう。

問題12: カテゴリ管理機能

書籍にカテゴリを追加できるように拡張してください。カテゴリのCRUD操作と、カテゴリによる書籍のフィルタリング機能を実装しましょう。

演習問題 解答例

初級問題解答

問題1解答例

from flask import Flask, jsonify

app = Flask(__name__)

books = [
    {"id": 1, "title": "Python入門", "author": "山田太郎"},
    {"id": 2, "title": "Flask Web開発", "author": "佐藤花子"}
]

@app.route('/books', methods=['GET'])
def get_books():
    return jsonify(books)

if __name__ == '__main__':
    app.run(debug=True)

問題2解答例

@app.route('/books/', methods=['GET'])
def get_book(book_id):
    book = next((b for b in books if b['id'] == book_id), None)
    if book:
        return jsonify(book)
    return jsonify({"error": "Book not found"}), 404

問題3解答例

from flask import request

@app.route('/books', methods=['GET'])
def get_books():
    author = request.args.get('author')
    if author:
        filtered_books = [b for b in books if b['author'] == author]
        return jsonify(filtered_books)
    return jsonify(books)

中級問題解答

問題4解答例

from flask import request

@app.route('/books', methods=['POST'])
def create_book():
    data = request.get_json()
    new_id = max([b['id'] for b in books]) + 1 if books else 1
    new_book = {
        "id": new_id,
        "title": data['title'],
        "author": data['author']
    }
    books.append(new_book)
    return jsonify(new_book), 201

問題5解答例

@app.route('/books/', methods=['PUT'])
def update_book(book_id):
    book = next((b for b in books if b['id'] == book_id), None)
    if not book:
        return jsonify({"error": "Book not found"}), 404

    data = request.get_json()
    if 'title' in data:
        book['title'] = data['title']
    if 'author' in data:
        book['author'] = data['author']

    return jsonify(book)

問題6解答例

@app.route('/books/', methods=['DELETE'])
def delete_book(book_id):
    global books
    book = next((b for b in books if b['id'] == book_id), None)
    if not book:
        return jsonify({"error": "Book not found"}), 404

    books = [b for b in books if b['id'] != book_id]
    return jsonify({"message": "Book deleted successfully"}), 200

問題7解答例

@app.errorhandler(404)
def not_found(error):
    return jsonify({
        "status": "error",
        "message": "Resource not found",
        "code": 404
    }), 404

@app.errorhandler(400)
def bad_request(error):
    return jsonify({
        "status": "error",
        "message": "Bad request",
        "code": 400
    }), 400

問題8解答例

def success_response(data, message=None, status_code=200):
    response = {
        "status": "success",
        "data": data
    }
    if message:
        response["message"] = message
    return jsonify(response), status_code

def error_response(message, status_code=400):
    return jsonify({
        "status": "error",
        "message": message,
        "code": status_code
    }), status_code

@app.route('/books/', methods=['GET'])
def get_book(book_id):
    book = next((b for b in books if b['id'] == book_id), None)
    if book:
        return success_response(book)
    return error_response("Book not found", 404)

問題9解答例

@app.route('/books', methods=['POST'])
def create_book():
    data = request.get_json()

    if not data or 'title' not in data or 'author' not in data:
        return error_response("Title and author are required", 400)

    if not data['title'].strip() or not data['author'].strip():
        return error_response("Title and author cannot be empty", 400)

    # 重複チェック
    if any(b['title'] == data['title'] for b in books):
        return error_response("Book with this title already exists", 400)

    new_id = max([b['id'] for b in books]) + 1 if books else 1
    new_book = {
        "id": new_id,
        "title": data['title'],
        "author": data['author']
    }
    books.append(new_book)
    return success_response(new_book, "Book created successfully", 201)

上級問題解答

問題10解答例

@app.route('/books', methods=['GET'])
def get_books():
    page = int(request.args.get('page', 1))
    per_page = int(request.args.get('per_page', 2))

    start_idx = (page - 1) * per_page
    end_idx = start_idx + per_page

    paginated_books = books[start_idx:end_idx]

    response = {
        "data": paginated_books,
        "pagination": {
            "page": page,
            "per_page": per_page,
            "total": len(books),
            "total_pages": (len(books) + per_page - 1) // per_page
        }
    }

    return success_response(response)

問題11解答例

import json
import os

DATA_FILE = 'books.json'

def load_books():
    global books
    if os.path.exists(DATA_FILE):
        with open(DATA_FILE, 'r', encoding='utf-8') as f:
            books = json.load(f)
    else:
        books = []

def save_books():
    with open(DATA_FILE, 'w', encoding='utf-8') as f:
        json.dump(books, f, ensure_ascii=False, indent=2)

# アプリ起動時にデータ読み込み
load_books()

@app.route('/books', methods=['POST'])
def create_book():
    data = request.get_json()

    if not data or 'title' not in data or 'author' not in data:
        return error_response("Title and author are required", 400)

    new_id = max([b['id'] for b in books]) + 1 if books else 1
    new_book = {
        "id": new_id,
        "title": data['title'],
        "author": data['author']
    }
    books.append(new_book)
    save_books()  # 変更を保存
    return success_response(new_book, "Book created successfully", 201)

問題12解答例

categories = [
    {"id": 1, "name": "プログラミング"},
    {"id": 2, "name": "データサイエンス"}
]

# 書籍データにカテゴリIDを追加
books = [
    {"id": 1, "title": "Python入門", "author": "山田太郎", "category_id": 1},
    {"id": 2, "title": "Flask Web開発", "author": "佐藤花子", "category_id": 1}
]

@app.route('/categories', methods=['GET'])
def get_categories():
    return success_response(categories)

@app.route('/categories//books', methods=['GET'])
def get_books_by_category(category_id):
    category_books = [b for b in books if b['category_id'] == category_id]
    return success_response(category_books)

@app.route('/categories', methods=['POST'])
def create_category():
    data = request.get_json()

    if not data or 'name' not in data:
        return error_response("Category name is required", 400)

    new_id = max([c['id'] for c in categories]) + 1 if categories else 1
    new_category = {
        "id": new_id,
        "name": data['name']
    }
    categories.append(new_category)
    return success_response(new_category, "Category created successfully", 201)