资讯动态

RESTful API设计实战:从资源概念到Python Flask完整落地

发布时间:2026/10/8 23:58:28 来源:尧图企业网站定制
在座各位写代码这么多年谁没有被API折磨过不管是调第三方接口、自己写后端服务还是给前端提供数据API设计的好坏直接决定了上下游的配合体验。而RESTful作为目前最主流的API风格很多人其实是会用但说不清能照着文档调通接口但真要自己设计一个规范的RESTful API心里就犯嘀咕资源路径怎么规划、状态码怎么选、要不要用PUT还是PATCH全是模糊地带。这篇文章我想用实际做项目的视角把RESTful API从原理到Python落地完整梳理一遍。不管你是刚入门Python的新手还是已经写过一阵子接口但想弄明白为什么这么设计的后端开发者这篇文章都适合你。我会从最核心的资源概念开始把URI设计、HTTP方法、状态码语义讲透再用Flask写一个完整的实战项目最后把我在调用各种API时踩过的坑整理成问题清单希望能帮你少走弯路。1. 先把RESTful这件事说清楚它到底在解决什么问题1.1 没有RESTful之前API是什么鬼样子先回忆一下最原始的API写法。早些年很多系统做接口喜欢在URL里塞动作比如/getUserById?id1、/deleteUser?id1、/createUser或者干脆用/user.do?methodquery这种风格。你说它有错吗也没错能用。但问题是接口多了以后动作和资源的边界越来越模糊文档稍微不完善调用方就得靠猜。更麻烦的是如果同一个资源需要新增一种操作你得再写一个接口名维护成本直线上升。我当时接手过一个老项目光用户相关的接口就有二十多个/queryUserForLogin、/queryUserForPage、/modifyUserPassword、/batchAddUsers……每个接口的名字都是动词名词的排列组合前端同学每调一个接口都要翻文档确认参数格式。这还不算最离谱的最离谱的是有些接口对外暴露的路径是一样的只靠一个隐藏的请求头字段区分逻辑排查线上问题的时候简直是灾难。1.2 RESTful的核心思想一切都是资源RESTful的提出本质上是把动作从URL里拿掉统一用HTTP方法表达行为URL里只保留资源。所谓资源就是你系统里要管理的东西用户、订单、文章、商品、文件……都是名词不是动词。你不再需要设计/createUser和/deleteUser这种接口而是统一用/users这个资源路径搭配POST表示新增、GET表示查询、PUT/PATCH表示修改、DELETE表示删除。这个转变看起来简单但它背后是一种思维方式的调整从接口是函数变成接口是资源的表述。函数式思维下你关心的是我要做什么资源式思维下你关心的是我要操作什么对象。这个区别很重要因为当你把关注点放在资源上接口的收敛性、一致性、可维护性都会上一个台阶。同一个资源无论新增、查询、修改、删除路径都只有一条动作靠HTTP方法表达语义就清晰得多。1.3 生活化类比餐厅点餐我用一个餐厅点餐的例子帮你理解。传统的旧式API风格就像你打电话跟服务员说我要催一下菜这是动作然后服务员问你要哪个桌的、催哪道菜。RESTful风格更像你和服务员之间有一套约定好的规则菜单资源里每一道菜都有固定编号URI你对这道菜可以点单POST、询问状态GET、改口味PATCH、退掉DELETE。服务员看到你用的动作词就知道你想干嘛根本不需要你解释这是一次什么行为。这种约定带来的直接好处是接口的使用者不需要记大量的动词接口名只需要知道资源有哪些、每个资源支持哪些操作即可。即便换了一个前端人员只要他理解RESTful约定上手成本会非常低。2. RESTful设计的关键规则与背后的为什么2.1 URI设计名词复数、层级关系、不要用动词RESTful的URI设计有几个基本约定虽然不是硬性标准但业界经过这么多年实践已经形成了一套比较公认的最佳写法。第一用名词复数表示资源集合。/users表示用户集合/users/123表示id为123的具体用户。为什么不直接/user说实话两种都能用但复数形式在语义上更统一GET /users天然就是获取所有用户GET /users/123是获取用户集合中的一个成员逻辑对称写代码的时候也方便统一处理。第二资源层级用斜杠表达从属关系。比如/users/123/orders表示id为123的用户名下的所有订单。但层级不要嵌套过深一般两层到三层就足够了超过三层说明你的资源划分可能有问题。我见过一个接口/users/123/orders/456/items/789这种设计虽然符合语义但实际用起来参数解析复杂缓存也不好做更重要的是它暗示了过多的耦合关系。如果你发现嵌套层级很深建议把后面的资源拆成顶层资源用查询参数表达关系比如/items?order_id456user_id123。第三不要在URI里出现动词。/users/create、/users/delete这类写法要避免动作已经由HTTP方法承担。唯一的例外是那些无法用标准CRUD表达的动作操作比如搜索、批量审核、导出有些人会用/users/search、/orders/export。这个可以接受但如果你想让API更纯粹可以用/users?actionsearch或者干脆用POST /users/search业界对此没有绝对定论关键是一旦项目里定了规则就保持一致。2.2 HTTP方法GET、POST、PUT、PATCH、DELETE怎么选这部分是新手最容易混淆的地方。我理一个清晰的判断逻辑GET查询资源幂等、无副作用参数放在查询字符串或路径里。只读操作必须用GET。POST创建资源非幂等。每次调用都会产生一个新资源或者触发一个动作。新增操作用POST。PUT整体替换资源幂等。客户端传入完整的资源表示服务端用这份内容完全覆盖目标资源。整体更新用PUT。PATCH部分更新资源幂等与否取决于实现但语义上是局部修改。只改某个字段用PATCH。DELETE删除资源幂等。删除操作用DELETE。这里最常踩的坑是PUT和PATCH的区别。很多项目图省事不管整体更新还是局部更新都用POST甚至GET加参数这种做法不规范也容易让服务器产生副作用。正确的做法是前端传整个文档对象就用PUT只传需要改的字段就用PATCH。有人会问POST和PUT都能创建资源为什么创造资源一定用POST因为POST是非幂等的每次POST都会生成一个全新资源PUT则要求客户端提供一个完整的资源标识服务端可以根据这个标识决定是创建还是更新。RESTful里PUT创建资源的场景PUT /users/123也不算罕见但日常业务里POST更直观、更常见。2.3 状态码HTTP状态码不是摆设状态码是RESTful接口语义的重要组成部分但也是最容易被敷衍的部分。很多后端同学无论什么情况都返回200然后JSON里放一个code: 500表示失败。这样做的坏处是HTTP层面的语义被架空下游的网关、日志、监控、缓存策略都没法基于状态码做精确处理。我的建议是既然用了HTTP协议就要尊重HTTP自己定义的状态码语义。说几个最常用的200 OK成功。GET查询成功、PUT整体更新成功都用它。201 Created创建成功POST新建资源后返回响应头里应带上新资源的Location。204 No Content删除成功或更新成功不需要返回body。400 Bad Request客户端参数错误、校验不通过。401 Unauthorized未认证Token缺失或无效。403 Forbidden已认证但没权限访问该资源。404 Not Found资源不存在。409 Conflict资源状态冲突比如创建用户时用户名已存在。422 Unprocessable Entity服务端理解请求但业务逻辑处理不了比如某个业务规则校验失败。500 Internal Server Error服务器内部错误。503 Service Unavailable服务过载或维护中。我见过一个真实场景某个接口因为上游数据库连接池满了直接返回200业务JSON里写code: 500。结果监控系统完全没发现异常直到用户反馈页面昨天开始频繁报错才定位到问题。这就是不尊重状态码语义的教训。2.4 为什么无状态这么重要RESTful强调无状态意思是服务端不保存客户端的会话状态每个请求都携带全部必要的上下文信息。这样做的好处非常实际服务器可以任意横向扩展同一用户的多个请求分到不同机器上处理不会有session丢失的问题故障机器可以直接摘除不影响任何在线会话。有人会担心这样我登录状态怎么保持答案是Token机制。客户端登录后拿到一个Token之后每个请求都带着它服务端校验Token即可。这个Token本身就是状态信息的载体服务端不需要存session从而实现了无状态。我在项目里常用的方案是JWT但要注意JWT有长度问题尤其请求头很大的场景后面在实战部分我会讲一个我踩过的坑。3. Python侧实战用Flask搭建一个规范的RESTful API3.1 技术选型为什么用Flask而不是Django或FastAPIPython写RESTful API的常用框架有三个Flask、Django配DRF、FastAPI。Flask轻量、灵活、上手快适合中小型项目或者想深入理解HTTP原理的开发者。它不会替你做太多决定你想要什么都可以自己加。Django DRF重量级自带ORM、Admin后台、认证体系适合大而全的项目。如果你的项目本身需要后台管理和复杂的模型关系Django开发效率很高。FastAPI性能高、自动生成OpenAPI文档、类型提示友好适合需要异步高性能的场景也是目前大模型相关API服务的常用选择。我这个实战项目用Flask来做因为它的体量最合适讲解RESTful的核心逻辑不会被框架自带的高级特性掩盖重点。等你自己用熟练之后再切换到FastAPI是很容易的事情。3.2 实战目标设计一个图书管理API我们就做一个经典场景图书管理系统。资源是书books要支持获取图书列表支持按作者筛选GET /books获取单本图书详情GET /books/{id}新增图书POST /books整体更新图书PUT /books/{id}局部更新图书信息比如只改价格PATCH /books/{id}删除图书DELETE /books/{id}为了聚焦API本身我用Python内置的数据结构模拟数据库不引入真实数据库这样你可以直接跑起来看效果。3.3 完整代码与逐段讲解先创建项目结构bookstore/ ├── app.py └── requirements.txtrequirements.txt只需要一行flask2.0。下面是app.py的完整代码from flask import Flask, request, jsonify, url_for app Flask(__name__) # 模拟数据库books 是一个列表元素是字典 books [] next_id 1 def find_book(book_id): 根据id查找图书返回图书字典或None for book in books: if book[id] book_id: return book return None def validate_book_data(data, partialFalse): 校验图书数据。 partialFalse 表示创建时需要完整校验 partialTrue 表示更新时只校验提供的字段。 errors [] if not isinstance(data, dict): return {errors: [请求体必须是一个JSON对象]} if not partial or title in data: title data.get(title) if not title or not isinstance(title, str): errors.append(title 字段必填且必须是字符串) if not partial or author in data: author data.get(author) if not author or not isinstance(author, str): errors.append(author 字段必填且必须是字符串) if not partial or price in data: price data.get(price) if isinstance(price, bool) or not isinstance(price, (int, float)): errors.append(price 字段必填且必须是数字) return {errors: errors} if errors else {} app.route(/books, methods[POST]) def create_book(): data request.get_json(silentTrue) result validate_book_data(data) if result.get(errors): return jsonify(result), 400 global next_id new_book { id: next_id, title: data[title], author: data[author], price: data[price], } next_id 1 books.append(new_book) # 201 Created并且返回新资源的完整信息 return jsonify(new_book), 201 app.route(/books, methods[GET]) def list_books(): author request.args.get(author) if author: filtered [b for b in books if b[author] author] return jsonify({items: filtered, total: len(filtered)}) return jsonify({items: books, total: len(books)}) app.route(/books/int:book_id, methods[GET]) def get_book(book_id): book find_book(book_id) if not book: return jsonify({error: 图书不存在}), 404 return jsonify(book) app.route(/books/int:book_id, methods[PUT]) def update_book(book_id): book find_book(book_id) if not book: return jsonify({error: 图书不存在}), 404 data request.get_json(silentTrue) # PUT是整体替换要求所有字段齐全 result validate_book_data(data, partialFalse) if result.get(errors): return jsonify(result), 400 book[title] data[title] book[author] data[author] book[price] data[price] return jsonify(book) app.route(/books/int:book_id, methods[PATCH]) def patch_book(book_id): book find_book(book_id) if not book: return jsonify({error: 图书不存在}), 404 data request.get_json(silentTrue) # PATCH是局部更新只校验传入的字段 result validate_book_data(data, partialTrue) if result.get(errors): return jsonify(result), 400 for key in (title, author, price): if key in data: book[key] data[key] return jsonify(book) app.route(/books/int:book_id, methods[DELETE]) def delete_book(book_id): global books book find_book(book_id) if not book: return jsonify({error: 图书不存在}), 404 books [b for b in books if b[id] ! book_id] # 删除成功返回204无需body return , 204 if __name__ __main__: app.run(debugTrue, port5000)代码本身不复杂但有几个细节值得专门说第一是request.get_json(silentTrue)。如果客户端传的不是合法JSONsilentTrue会让它返回None而不是直接抛400异常这样我们可以在自己的业务逻辑里统一处理错误提示。否则Flask会在框架层面返回一个HTML格式的400错误页跟API场景不搭。第二是校验函数的设计。我把校验逻辑拆成一个公共函数用partial参数区分创建和更新两种场景。PUT要求完整校验PATCH只校验传入的字段。这个设计可以避免在多个路由里重复写校验代码也保证了行为的一致性。第三是状态码的处理。新增返回201删除返回204参数错误返回400找不到资源返回404。这些细节在演示项目里看起来很细碎但到了生产环境它们会直接影响前端联调、监控告警和日志分析的准确性。3.4 用curl实测接口代码写完了跑起来python app.py然后在另一个终端里做几个测试# 新增一本书 curl -X POST http://127.0.0.1:5000/books \ -H Content-Type: application/json \ -d {title:深入浅出RESTful API,author:张三,price:59.9} # 新增第二本书 curl -X POST http://127.0.0.1:5000/books \ -H Content-Type: application/json \ -d {title:Python编程实战,author:李四,price:69.0} # 查询图书列表 curl http://127.0.0.1:5000/books # 按作者筛选 curl http://127.0.0.1:5000/books?author张三 # 查询单本 curl http://127.0.0.1:5000/books/1 # PATCH局部更新只改价格 curl -X PATCH http://127.0.0.1:5000/books/1 \ -H Content-Type: application/json \ -d {price:49.9} # 删除 curl -X DELETE http://127.0.0.1:5000/books/1 -i这里我发现一个很多人会忽略的点PATCH接口在测试时很容易被写成PUT。客户端同学、联调工具甚至自动生成的SDK都可能把局部修改的方法统一发成PUT。所以设计API时如果项目允许可以同时兼容这两个方法或者至少在文档中把区别写清楚。我的习惯是两者都支持但业务上优先引导使用PATCH做局部更新。3.5 分页、排序、筛选的通用写法真实项目里的列表接口不可能像演示代码那样直接把所有数据返回数据量大一点就会压垮服务。分页是RESTful API的必修课。常见的做法是支持page和page_size两个查询参数响应中返回items、total、page、total_pages等元信息。改造一下列表接口代码片段app.route(/books, methods[GET]) def list_books(): author request.args.get(author) page request.args.get(page, default1, typeint) page_size request.args.get(page_size, default20, typeint) filtered books if author: filtered [b for b in books if b[author] author] start (page - 1) * page_size end start page_size page_items filtered[start:end] return jsonify({ items: page_items, total: len(filtered), page: page, page_size: page_size, total_pages: (len(filtered) page_size - 1) // page_size })这里有三个细节要注意page和page_size要用typeint做显式类型转换。Flask的request.args.get默认返回的是字符串不转类型的话后面做减法运算会直接抛类型错误。total_pages的计算用向上取整的方式。(total page_size - 1) // page_size是经典的整数除法技巧避免了引入浮点数。page_size要设置上限。经验值是不超过100防止有人传个page_size1000000把全表数据一次性拖走。我在生产项目里遇到过这种调用一个看似温和的GET接口直接把数据库查崩了。4. 客户端调用与调试requests库的实操细节4.1 requests库的基本用法和常见误区写好了API自然要学会怎么调用它。Python生态里最常用的HTTP客户端库就是requests它足够简单也足够强大。基础用法不展开说了这里重点讲我在实际项目中反复踩过的坑。第一个坑不设超时。很多新手写requests.get(url)不传timeout默认情况下这个请求会无限等待。如果你的服务端因为某些原因一直不响应你的脚本就挂死在那边了。正经项目里的HTTP调用必须设置超时而且要区分连接超时和读取超时import requests resp requests.get( http://127.0.0.1:5000/books, timeout(3, 10) # 连接超时3秒读取超时10秒 )第二个坑不检查HTTP状态码。很多代码这么写resp requests.get(url); data resp.json()。如果返回的是404或500resp.json()可能抛出一个JSON解析异常或者返回一个你完全没预料到的错误结构导致后面处理逻辑全部乱套。正确姿势是先看状态码再决定如何解析resp requests.get(url, timeout5) if resp.status_code 200: data resp.json() elif resp.status_code 404: # 处理资源不存在 pass else: # 记录日志报警 pass第三个坑忽略Session复用。如果你需要连续调用多个接口每个请求都单独用requests.get的话底层会重复建立TCP连接性能很差。正确做法是用requests.Session()复用连接session requests.Session() resp1 session.get(http://127.0.0.1:5000/books, timeout5) resp2 session.get(http://127.0.0.1:5000/books/1, timeout5)Session也会自动保存Cookie这在调试需要登录态的接口时非常有用。4.2 一个真实场景调用大模型API的RESTful实践现在很多大模型服务都是通过RESTful API对外提供能力的。调用方式本质上和我们上面写的图书API没有任何区别发送HTTP请求、携带认证信息、传递参数、解析响应。我用DeepSeek的API调用作为例子它和OpenAI的接口规范非常像你可以直观感受一下import requests def call_llm(prompt): api_url https://api.deepseek.com/chat/completions api_key 你的API_KEY headers { Authorization: fBearer {api_key}, Content-Type: application/json } payload { model: deepseek-chat, messages: [ {role: system, content: 你是一个编程助手。}, {role: user, content: prompt} ], max_tokens: 2048, temperature: 0.7 } resp requests.post(api_url, jsonpayload, headersheaders, timeout(10, 60)) if resp.status_code 200: data resp.json() return data[choices][0][message][content] else: # 这里值得注意大模型API出错时返回的body里通常有error字段描述原因 error_info resp.json().get(error, {}) print(fAPI调用失败状态码{resp.status_code}原因{error_info}) return None print(call_llm(用Python写一个斐波那契函数))这段代码有几个地方特别容易出问题认证方式。大模型API普遍采用Authorization: Bearer token的方式这也是RESTful API里常见的认证模式。Token本身不放在URL里避免被日志系统记录下来。系统提示词。messages数组里第一条通常放system角色它用来设定模型的行为之后再放用户消息。如果你调不通或者返回内容跑偏先检查消息结构是否规范。max_tokens参数。这是控制模型生成内容长度的上限不是输入长度。有次我把用户的问题文本长度误以为受max_tokens限制结果模型总是中途截断排查了半天才明白是我理解错了。4.3 免费大模型API的注意点很多同学一开始想练手又不想充值就会去找各种免费的大模型API。免费额度这个东西我在项目里用过不少有几个经验值得分享第一免费额度通常有QPS限制。可能一分钟只允许调用几次或者一天几百次。如果你写个脚本循环调用很快就会被限流。加个简单的time.sleep()可以有效缓解。第二免费额度的认证信息经常是临时有效的。有些平台给的API Key过几天就失效了排查半天发现是Key过期血泪教训。第三不同提供方的API路径不同。有的路径是/v1/chat/completions有的是/v1/completions还有的是/api/chat。接口结构不统一这是调用各家大模型API时最头疼的一件事。如果你要对接多家平台建议在公司内封装一层统一的调用接口底层适配不同厂商。5. 常见问题与排查技巧实录5.1 Docker API权限问题permission denied热词里有一条 permission denied while trying to connect to the docker api at unix:///var/r...这是Linux环境下经常遇到的。很多Python脚本想通过Docker API管理容器结果一运行就报这个错。根本原因是当前用户没有权限访问Docker的socket文件。解决方法有两种# 方法一把当前用户加入docker组然后重新登录终端 sudo usermod -aG docker $USER # 方法二临时用sudo运行 sudo python3 your_script.py用方法一更优雅但要记得重新登录终端或重启一下shell才生效。如果你是公司内网环境安全策略比较严格可能连透明的docker.sock都不允许你访问那就只能用客户端的API方式比如配置DOCKER_HOST的TCP模式但要注意开启TLS认证别把Docker控制端口裸奔暴露。5.2 大模型API上下文长度超限的问题api error: 400 this models maximum context length is 1048576 tokens——这条报错这两年越来越常见。尤其是像DeepSeek这类上下文窗口特别长的模型因为窗口够长我们很容易忽略历史消息的累积。上下文长度指的是输入和输出的总token数不能超过模型上限。如果你在做多轮对话每轮都把之前所有对话重新发一次那么几轮下来token就爆了。解决办法是维护一个消息窗口只保留最近若干轮对话超过数量就删掉最早的或者对早期历史做摘要用摘要文本替代完整记录。我写过一个简单的滑动窗口def trim_messages(messages, max_messages20): 只保留最近20条消息 if len(messages) max_messages: return messages[-max_messages:] return messages这个方案简单粗暴但有效。更优雅的做法是用tokenizer计算真实token数超过阈值再截断但那个成本稍高。5.3 API调用量超限限流与配额很多第三方API比如短信服务、地图服务、大模型API都有限流配置。我之前做过一个项目调用阿里云短信接口总是发不出去日志里什么都没显示后来发现是当天短信服务的使用量达到了配额上限。排查这种问题思路是这样查HTTP状态码。429表示被限流了这通常是瞬时判断如果状态码是200但业务码报错可能不是限流而是配额耗尽。看响应体的错误码。不同平台有自己的错误码体系一定要把响应体打日志里别只记录状态码。区分QPS和日配额。QPS超了一般过几秒自动恢复日配额超了通常要等第二天零点重置。我当时是把所有第三方API调用的请求参数和响应body都记了结构化日志出问题时直接按时间线查效率非常高。给所有API调用加日志这个习惯建议一早就养好别等出问题才想起来加。5.4 一个隐蔽的坑JWT请求头过大有段时间我做了一个内部系统前端总是反映登录态会突然丢失登录后过一会请求就401。排查了很久最后发现是用户信息JWT的载荷里塞了太多东西Token体积超过了很多网关的请求头大小限制。常见的Nginx默认large_client_header_buffers有限制过大的Authorization头会被TLS层直接丢弃服务端收到的请求里压根没有Token所以返回401。解决方案是把JWT里的非必要字段去掉只保留user_id、role这些核心信息其他数据查库获取。这个坑提醒我设计API的时候对请求头大小要有意识别在Token里塞没用的东西。5.5 API文档的重要性别让接口成为黑盒最后说一个跟代码关系不大但影响很大的问题——API文档。我见过太多项目代码写得还行但文档约等于没有联调全靠问。这里推荐一个工具使用OpenAPISwagger规范描述接口Python项目里配置起来很方便如果你用的是FastAPI更是自带文档页面。就算你用的是Flask也可以用flask-smorest或apispec这类库自动生成Swagger文档。我个人的经验是API文档在写接口的时候就同步更新而不是全部写完再补。否则过一个月你连自己设计的参数含义都忘了。6. 最后的个人体会做API设计这行做得越久越会明白RESTful并不是一套必须逐字遵守的教条而是一种减少认知负担的工程风格。关键不在于是不是完全符合RESTful规范而在于你的接口是否有清晰统一的资源边界、状态码语义是否准确、文档是否及时、日志是否充分。风格不统一的项目时间越久维护成本越像滚雪球一样膨胀。我实际带项目的过程中发现真正让人头疼的往往不是框架选型、不是代码性能而是接口语义在不同团队之间被理解成各种样子。所以如果你现在刚开始设计自己的第一个RESTful API我的建议是先严格按照这篇提到的规则来哪怕显得死板也要把资源、方法、状态码一一对齐。等你有足够的经验之后你自然会知道哪些地方该做取舍哪些约定在特定场景下该让位于工程效率。最后分享一个小技巧定义路由时把每个接口的行为在注释里写明做什么、成功返回什么状态码、失败返回什么状态码形成习惯。半年后再回头看你会感谢当时的自己。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑