第7章 7.2 RESTful API 设计


🎯 开场 3 分钟:为什么要学这个?

上一章我们搞懂了 HTTP 协议——浏览器和服务器是怎么「对话」的。你现在应该知道:请求是客户端发起的,响应是服务器返回的,header 里藏着各种元信息。

但有个问题:上一章我们写的代码,都是浏览器主动访问服务器固定页面。如果我想做一件事——让程序(比如 App、小程序、另一个服务器)能够「命令」我的服务器做事呢?

举个例子:

你打开一个待办清单 App,点一下「完成」按钮,App 马上显示 ✅ 已完成。这背后发生了什么?

答案是:你的 App 偷偷给服务器发了一条「命令」,告诉它「把第 42 号待办标记为已完成」

这种「程序对程序」的命令传递方式,就是 API(应用程序接口)。而 RESTful API 是目前最流行的「命令格式约定」。

学完这一章,你就能写出这样的接口——让任何程序都能「指挥」你的 Python 程序做事。


🧱 基础 25 分钟:核心概念

什\n\nSimple tech illustration expla\n\nAI comic creation scene, creat\n\n么是 RESTful API?——「餐厅点餐」的类比

想象你走进一家餐厅:

  • 服务员(API):站在你和厨房之间,接收你的「命令」
  • 菜单(REST 规则):你只能说菜单上有的词汇,不能乱点
  • 厨房(服务器后端):真正干活的地方

RESTful 就是这样一套「点餐规则」:
- 你不能说「给我做个宫保鸡丁,要加花生,不要辣椒」,你只能说「点宫保鸡丁」
- 服务员听懂了,去厨房,厨房做好,端回来给你

说白了:RESTful 就是一套「怎么用 HTTP 命令来操作数据」的约定。

核心概念 1:URL 是「名词」,不是「动词」

❌ 旧式 API(像在说话):

GET /getUser?id=1          # "去拿用户"
POST /deleteUser?id=1      # "删除用户"

✅ RESTful API(像在点名):

GET    /users/1            # "我要看 1 号用户"
DELETE /users/1            # "删掉 1 号用户"
POST   /users              # "新建一个用户"
PUT    /users/1            # "更新 1 号用户"

生活类比:就像你去图书馆,你不说「帮我找书」,而是说「我要借《活着》」——你点的是东西本身,不是动作

核心概念 2:HTTP 方法是「动作」

方法 含义 类比
GET 读取/查看 翻看菜单
POST 新建 点新菜
PUT 完整更新 换一道菜
DELETE 删除 退菜

核心概念 3:状态码是「回报」

服务器处理完你的命令,会返回一个「几号状态」:

状态码 含义 什么时候用
200 成功 正常返回数据
201 已创建 新建数据成功
400 请求错误 你发的命令有语法问题
404 找不到 要的东西不存在
500 服务器挂了 厨房着火了

生活类比:就像点餐后服务员喊「67 号!」——200 就是「您的菜好了」,400 就是「您点的这道菜我们没有」,500 就是「厨房今天停电了」。

核心概念 4:JSON 是「餐具」

RESTful API 用 JSON 格式来传递数据。JSON 就像一个标准化的「餐盘」——不管你点的是什么菜,都用同一种盘子装。

# Python 里的 JSON 就是字典格式
user = {
"name": "张三",
"age": 25,
"hobby": ["跑步", "读书"]
}

第一个可运行的 API——用 Flask 搭一个「留言板」

为什么要用 Flask? 它是 Python 里最轻量的 Web 框架,就像「自行车」——简单、好学、够用。

安装 Flask:

pip install flask

创建 app.py,写一个最简单的 API:

from flask import Flask, jsonify, request

app = Flask(__name__)

# 模拟一个数据库(就是一张「便利贴」)
messages = []

@app.route('/messages', methods=['GET'])
def get_messages():
"""获取所有留言"""
return jsonify(messages)

@app.route('/messages', methods=['POST'])
def add_message():
"""添加一条留言"""
data = request.get_json()  # 接收 JSON 数据
messages.append(data)
return jsonify({"status": "ok", "message": "留言已添加"})

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

运行:

python app.py

测试(另开一个终端):

curl -X GET http://localhost:5000/messages
curl -X POST http://localhost:5000/messages \
 -H "Content-Type: application/json" \
 -d '{"user": "小明", "content": "今天天气真好"}'

解释:这段代码做了三件事:
1. app = Flask(__name__) —— 创建一个 Flask 应用实例
2. @app.route('/messages', methods=['GET']) —— 定义「看留言」的入口
3. request.get_json() —— 读取用户发来的 JSON 数据


🔥 实战 35 分钟:3 个递进的小项目

项目 1(5 分钟):个人名片管理——CRUD 全家桶

需求:做一个「名片夹」API,能增删改查名片。

from flask import Flask, jsonify, request

app = Flask(__name__)

# 模拟数据库
cards = [
{"id": 1, "name": "张三", "phone": "13800138000"},
{"id": 2, "name": "李四", "phone": "13900139000"},
]

@app.route('/cards', methods=['GET'])
def get_cards():
"""获取所有名片"""
return jsonify(cards)

@app.route('/cards/<int:card_id>', methods=['GET'])
def get_card(card_id):
"""获取单张名片"""
card = next((c for c in cards if c["id"] == card_id), None)
if card:
    return jsonify(card)
return jsonify({"error": "名片不存在"}), 404

@app.route('/cards', methods=['POST'])
def create_card():
"""新建名片"""
data = request.get_json()
new_id = max(c["id"] for c in cards) + 1
new_card = {"id": new_id, "name": data["name"], "phone": data["phone"]}
cards.append(new_card)
return jsonify(new_card), 201

@app.route('/cards/<int:card_id>', methods=['DELETE'])
def delete_card(card_id):
"""删除名片"""
global cards
cards = [c for c in cards if c["id"] != card_id]
return jsonify({"status": "deleted"})

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

测试方法

# 查看所有名片
curl http://localhost:5000/cards

# 查看 ID 为 1 的名片
curl http://localhost:5000/cards/1

# 新建一张名片
curl -X POST http://localhost:5000/cards \
 -H "Content-Type: application/json" \
 -d '{"name": "王五", "phone": "13700137000"}'

# 删除 ID 为 1 的名片
curl -X DELETE http://localhost:5000/cards/1

预期输出:返回对应的 JSON 数据。

一句话解释:这个项目展示了 RESTful 的核心——用不同的 HTTP 方法操作同一个资源 /cards


项目 2(15 分钟):天气查询小工具——对接真实 API

需求:做一个天气查询工具,用户输入城市名,我们调用「天气 API」获取数据并返回。

这个项目用到了真实的外部 API,体验一下「程序对程序」调用是什么感觉。

首先去 心知天气OpenWeatherMap 申请一个免费 API Key(就像去餐厅要会员卡)。

from flask import Flask, jsonify, request
import requests

app = Flask(__name__)

# 这里填你的 API Key(免费版够用)
API_KEY = "your_api_key_here"
BASE_URL = "http://api.openweathermap.org/data/2.5/weather"

@app.route('/weather', methods=['GET'])
def get_weather():
"""查询天气"""
city = request.args.get('city', 'Beijing')  # 默认北京

# 调用外部 API
params = {
    'q': city,
    'appid': API_KEY,
    'units': 'metric'  # 摄氏温度
}

try:
    response = requests.get(BASE_URL, params=params)
    data = response.json()

    if response.status_code == 200:
        result = {
            "city": data["name"],
            "temperature": data["main"]["temp"],
            "description": data["weather"][0]["description"],
            "humidity": data["main"]["humidity"]
        }
        return jsonify(result)
    else:
        return jsonify({"error": data.get("message", "查询失败")}), 400

except Exception as e:
    return jsonify({"error": str(e)}), 500

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

测试方法

curl "http://localhost:5000/weather?city=Shanghai"

预期输出

{
"city": "Shanghai",
"temperature": 22.5,
"description": "few clouds",
"humidity": 65
}

一句话解释:这个项目展示了 RESTful API 的真实用途——你的程序作为「中间人」,一边接收用户请求,一边去调用别人的 API


项目 3(15 分钟):待办清单 API——数据持久化 + 基础鉴权

需求:做一个待办清单 API,支持创建、查看、完成、删除,还能把数据保存到文件里(重启不丢数据)。

from flask import Flask, jsonify, request
import json
import os

app = Flask(__name__)
DATA_FILE = 'todos.json'

# 加载数据(如果文件存在)
def load_todos():
if os.path.exists(DATA_FILE):
    with open(DATA_FILE, 'r', encoding='utf-8') as f:
        return json.load(f)
return []

# 保存数据
def save_todos(todos):
with open(DATA_FILE, 'w', encoding='utf-8') as f:
    json.dump(todos, f, ensure_ascii=False, indent=2)

todos = load_todos()

@app.route('/todos', methods=['GET'])
def get_todos():
"""获取所有待办"""
return jsonify(todos)

@app.route('/todos', methods=['POST'])
def create_todo():
"""创建待办"""
data = request.get_json()
new_id = max([t["id"] for t in todos], default=0) + 1
new_todo = {
    "id": new_id,
    "title": data["title"],
    "done": False
}
todos.append(new_todo)
save_todos(todos)
return jsonify(new_todo), 201

@app.route('/todos/<int:todo_id>', methods=['PUT'])
def update_todo(todo_id):
"""更新待办(标记完成)"""
data = request.get_json()
for todo in todos:
    if todo["id"] == todo_id:
        todo["done"] = data.get("done", todo["done"])
        save_todos(todos)
        return jsonify(todo)
return jsonify({"error": "待办不存在"}), 404

@app.route('/todos/<int:todo_id>', methods=['DELETE'])
def delete_todo(todo_id):
"""删除待办"""
global todos
todos = [t for t in todos if t["id"] != todo_id]
save_todos(todos)
return jsonify({"status": "deleted"})

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

测试方法

# 创建待办
curl -X POST http://localhost:5000/todos \
 -H "Content-Type: application/json" \
 -d '{"title": "买牛奶"}'

# 查看所有
curl http://localhost:5000/todos

# 标记完成
curl -X PUT http://localhost:5000/todos/1 \
 -H "Content-Type: application/json" \
 -d '{"done": true}'

# 删除
curl -X DELETE http://localhost:5000/todos/1

预期输出:JSON 格式的待办数据,todos.json 文件里也会保存一份。

一句话解释:这个项目加入了文件持久化——数据不会因为程序重启而丢失,这才是真正能用的工具。


💪 进阶 20 分钟:常见坑 + 性能小贴士

坑 1:POST 和 PUT 分不清

错误:用 POST 做更新

# 语义错误:POST 应该是「新建」,不是「更新」
@app.route('/users/<int:user_id>', methods=['POST'])
def update_user(user_id):
...

正确:更新用 PUT

@app.route('/users/<int:user_id>', methods=['PUT'])
def update_user(user_id):
...

坑 2:状态码乱返回

错误:删除成功返回 200

def delete_user(user_id):
user = find_user(user_id)
delete(user)
return jsonify({"status": "ok"})  # 语义不清

正确:按 REST 规范返回 204(无内容)

def delete_user(user_id):
delete(user)
return '', 204  # 明确表示「删除成功,没有返回内容」

坑 3:JSON 数据读取方式搞混

# 如果前端发来的是 JSON body,用这个:
data = request.get_json()

# 如果前端发来的是 URL 参数 ?name=张三,用这个:
name = request.args.get('name')

# 如果是表单提交(form-data),用这个:
name = request.form.get('name')

坑 4:不处理异常,服务器直接崩溃

危险代码

@app.route('/users/<int:user_id>')
def get_user(user_id):
user = db.query(user_id)  # 如果 user_id 无效,直接报错 500
return jsonify(user)

正确做法

@app.route('/users/<int:user_id>')
def get_user(user_id):
user = db.query(user_id)
if not user:
    return jsonify({"error": "用户不存在"}), 404
return jsonify(user)

坑 5:调试模式,生产环境忘记关

# ❌ 上线后 debug=True 很危险,别人能看到你的代码
app.run(debug=True, port=5000)

# ✅ 生产环境
app.run(debug=False, port=5000)

性能小贴士:使用 jsonify 而不是 str(dict)

# ❌ 慢,而且 Content-Type 不对
return str({"status": "ok"})

# ✅ 快,自动设置正确的 Content-Type
return jsonify({"status": "ok"})

调试技巧:加日志

import logging

# 设置日志
logging.basicConfig(level=logging.INFO)

@app.route('/todos', methods=['POST'])
def create_todo():
data = request.get_json()
logging.info(f"收到新建待办请求: {data}")
# ... 业务逻辑
logging.info(f"新建成功: {new_todo}")
return jsonify(new_todo), 201

✏️ 练习题 + 作业题

练习题(10 分钟)

练习 1(2 分钟):改个端口
- 输入:在项目 1 代码中,把端口从 5000 改成 8000
- 预期输出:curl 命令能访问 http://localhost:8000/cards
- 提示:只改 app.run() 那行的 port 参数

练习 2(2 分钟):加个「学历」字段
- 输入:在项目 1 的 /cards POST 端点,添加 "education": "大学" 字段
- 预期输出:新建名片时能保存学历
- 提示:在 create_card 函数里,加一个 data.get("education", "")

练习 3(3 分钟):换个外部 API
- 输入:用「心知天气」API(https://www.seniverse.com/)替代项目 2 的 OpenWeatherMap
- 预期输出:同样能查询天气,只是 URL 和参数不同
- 提示:看心知天气的文档,把 BASE_URLparams 改掉

练习 4(3 分钟):项目 2 + 3 串起来
- 输入:在项目 3 的待办清单里,每创建一个待办,就自动查询一下「创建地点」的天气
- 预期输出:创建待办时返回待办信息 + 当地天气
- 提示:参考项目 2 的天气查询函数,在 create_todo 里调用它

练习 5(挑战题,看截图分析)
- 题目:运行项目 3 时,访问 curl -X POST http://localhost:5000/todos -d '{"title": "test"}' 报错
- 输入:收到的错误信息是「415 Unsupported Media Type」
- 预期输出:改成正确的方式后能成功创建
- 提示:检查 header,Content-Type 很重要


作业题(30 分钟 - 2 小时)

作业:做一个「图书管理 RESTful API」

需求描述:做一个图书馆管理系统 API,支持对图书的 CRUD 操作。

功能点
1. GET /books —— 查看所有图书
2. POST /books —— 添加新书(书名、作者、 ISBN)
3. GET /books/<id> —— 查看某本书
4. PUT /books/<id> —— 更新图书信息
5. DELETE /books/<id> —— 删除图书

加分项(选做):
1. 添加「借阅」功能:POST /books/<id>/borrow,标记某本书被借出
2. 数据保存到 JSON 文件

验收标准
- 能跑起来
- 用 curl 测试每个接口都有正确返回
- 代码有注释

提交方式:评论区贴代码或 GitHub 链接


📚 总结 + 资源

本文学了 3 件事
1. RESTful 是一种「用 URL 表示资源、用 HTTP 方法表示动作」的 API 设计规范
2. Flask 是 Python 里最简单好用的 Web 框架,10 行代码就能搭一个 API
3. 真实项目中,数据持久化和错误处理比功能本身更重要

延伸学习资源
- Flask 官方文档 —— 权威且有中文
- 《Python 网络数据采集》—— 教你用 Python 抓网页数据
- RESTful API 设计最佳实践 —— 深入理解 REST


互动钩子:你在工作中需要对接别人的 API 吗?遇到过什么坑?评论区聊聊,老粉优先回复!


预告:下一章我们要解决一个很现实的问题——「如果 API 报错了怎么办?服务器崩了怎么办?」下一章「异常处理与日志」,教你写出「打不死」的程序。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。