文章

Flask - 待辦事項 (To-Do) API

目標

  • 構建一個完整的待辦事項 API
  • 應用路由、HTTP 方法、請求處理、錯誤處理和模擬數據存儲
  • 確保 API 結構清晰且易於使用

項目需求

  • 端點
    • GET /api/v1/todos:獲取所有任務
    • GET /api/v1/todos/<id>:獲取單個任務
    • POST /api/v1/todos:創建新任務
    • PUT /api/v1/todos/<id>:更新任務
    • DELETE /api/v1/todos/<id>:刪除任務
  • 任務字段
    • id:唯一標識符(整數)
    • title:任務標題(字符串)
    • completed:完成狀態(布林值,默認為 False)

步驟

  1. 準備環境

    • 使用現有的 flask_api/ 項目結構。如果需要新建,參考第 4 天的結構:
      1
      2
      3
      4
      5
      6
      7
      8
      
      flask_api/
      ├── app/
      │   ├── __init__.py
      │   ├── routes/
      │   │   ├── __init__.py
      │   │   └── todos.py
      │   └── config.py
      └── run.py
      
    • 激活虛擬環境:
      1
      2
      
      # Windows: flask_api_env\Scripts\activate
      # macOS/Linux: source flask_api_env/bin/activate
      
  2. 配置應用

    • app/init.py(保持錯誤處理並註冊新藍圖):

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      
      from flask import Flask, jsonify
      from .routes.todos import todos_bp
      
      def create_app():
          app = Flask(__name__)
          app.config.from_object('app.config.Config')
      
          # 註冊藍圖
          app.register_blueprint(todos_bp, url_prefix='/api/v1')
      
          # 錯誤處理器
          @app.errorhandler(404)
          def not_found(error):
              return jsonify({
                  'error': 'Not Found',
                  'message': str(error)
              }), 404
      
          @app.errorhandler(400)
          def bad_request(error):
              return jsonify({
                  'error': 'Bad Request',
                  'message': str(error)
              }), 400
      
          @app.errorhandler(500)
          def internal_error(error):
              return jsonify({
                  'error': 'Internal Server Error',
                  'message': 'Something went wrong on our end'
              }), 500
      
          return app
      
    • app/config.py(保持不變):
      1
      2
      
      class Config:
          DEBUG = True
      
    • run.py(保持不變):

      1
      2
      3
      4
      5
      6
      
      from app import create_app
      
      app = create_app()
      
      if __name__ == '__main__':
          app.run()
      
  3. 實現待辦事項路由

    • app/routes/todos.py

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      
      from flask import Blueprint, jsonify, request, abort
      
      todos_bp = Blueprint('todos', __name__)
      
      # 模擬數據庫
      todos_db = {
          1: {'id': 1, 'title': 'Learn Flask', 'completed': False},
          2: {'id': 2, 'title': 'Build an API', 'completed': True}
      }
      
      # GET - 獲取所有任務
      @todos_bp.route('/todos', methods=['GET'])
      def get_todos():
          completed = request.args.get('completed', type=lambda x: x.lower() == 'true')
          filtered_todos = list(todos_db.values())
          if completed is not None:
              filtered_todos = [todo for todo in filtered_todos if todo['completed'] == completed]
          return jsonify({'todos': filtered_todos})
      
      # GET - 獲取單個任務
      @todos_bp.route('/todos/<int:todo_id>', methods=['GET'])
      def get_todo(todo_id):
          todo = todos_db.get(todo_id)
          if not todo:
              abort(404, description='Todo not found')
          return jsonify(todo)
      
      # POST - 創建新任務
      @todos_bp.route('/todos', methods=['POST'])
      def create_todo():
          if not request.is_json:
              abort(400, description='Request must be JSON')
          data = request.get_json()
          if 'title' not in data:
              abort(400, description='Missing title')
          new_id = max(todos_db.keys(), default=0) + 1
          new_todo = {
              'id': new_id,
              'title': data['title'],
              'completed': data.get('completed', False)  # 默認為 False
          }
          todos_db[new_id] = new_todo
          return jsonify(new_todo), 201
      
      # PUT - 更新任務
      @todos_bp.route('/todos/<int:todo_id>', methods=['PUT'])
      def update_todo(todo_id):
          if todo_id not in todos_db:
              abort(404, description='Todo not found')
          if not request.is_json:
              abort(400, description='Request must be JSON')
          data = request.get_json()
          todo = todos_db[todo_id]
          if 'title' in data:
              todo['title'] = data['title']
          if 'completed' in data and isinstance(data['completed'], bool):
              todo['completed'] = data['completed']
          return jsonify(todo), 200
      
      # DELETE - 刪除任務
      @todos_bp.route('/todos/<int:todo_id>', methods=['DELETE'])
      def delete_todo(todo_id):
          if todo_id not in todos_db:
              abort(404, description='Todo not found')
          del todos_db[todo_id]
          return jsonify({'message': 'Todo deleted'}), 200
      
  4. 運行應用

    • 運行:
      1
      
      python run.py
      
  5. 測試 API

    • 使用 Postman 測試以下端點:
      • GET /api/v1/todos
        • 預期響應:{"todos": [{"id": 1, "title": "Learn Flask", "completed": false}, {"id": 2, "title": "Build an API", "completed": true}]}
      • GET /api/v1/todos?completed=true
        • 預期響應:{"todos": [{"id": 2, "title": "Build an API", "completed": true}]}
      • GET /api/v1/todos/1
        • 預期響應:{"id": 1, "title": "Learn Flask", "completed": false}
      • POST /api/v1/todos
        • Body:{"title": "Test API", "completed": false}
        • 預期響應:{"id": 3, "title": "Test API", "completed": false}
      • PUT /api/v1/todos/1
        • Body:{"completed": true}
        • 預期響應:{"id": 1, "title": "Learn Flask", "completed": true}
      • DELETE /api/v1/todos/2
        • 預期響應:{"message": "Todo deleted"}
      • 錯誤測試
        • POST 無 title:應返回 400。
        • GET/DELETE 無效 ID:應返回 404。
  6. 項目驗收

    • 確保所有端點正常工作。
    • 檢查錯誤處理是否一致返回 JSON 格式。
  7. 作業

    • 添加一個查詢參數 title 到 GET /todos,支持按標題模糊匹配(提示:使用 str.contains)。
    • (可選)創建一個簡單的前端頁面(HTML+JavaScript),調用 API 顯示任務列表。

注意事項

  • 數據存儲仍是記憶體中的字典,重啟後會丟失。
  • 確保輸入驗證嚴謹,例如 completed 必須是布林值。

本文章以 CC BY 4.0 授權