API 设计哲学 API 设计哲学完整流程大白话版 一、先搞懂API 设计哲学是个啥 大白话API 就是后端和前端或别的服务之间的菜单。设计哲学就是怎么写这个菜单——让别人一看就懂、好点、不容易点错、以────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 后加菜也不影响老客户。 好 API 的6个标准1.一致性-命名、格式、行为都要统一不能这个接口返回 user_id那个返回 userId2.可预测-看到 URL 就猜得到干啥3.简单-能5个字段说清楚的别写20个4.向后兼容-改了不能把老客户搞崩5.错误友好-报错要告诉人怎么修6.安全-默认不信任任何人---二、API 设计完整流程从无到有1.需求分析 →搞清楚谁用、用来干啥 ↓2.资源建模 →把业务抽象成名词资源 ↓3.定义契约 →写 OpenAPI/Swagger 文档先文档后代码 ↓4.URL 设计 →/users/{id}/orders 这种 ↓5.方法状态码 →GET/POST/PUT/DELETE200/400/404↓6.请求/响应格式 →JSON 结构、分页、错误结构 ↓7.鉴权限流 →Token、Rate Limit ↓8.版本管理 →/v1/users老版本别立刻删 ↓9.编码实现 →FastAPI/Express/Spring ↓10.测试文档 →Postman 集合、自动生成文档 ↓11.监控灰度 →指标埋点、灰度发布---三、核心设计原则大白话讲透1.RESTful把一切当资源错的写法动词到处飞 GET/getUserList POST/createUser POST/deleteUserById?id1POST/updateUserName 对的写法名词HTTP 动词 GET/users # 查列表 GET/users/1# 查单个 POST/users # 新建 PUT/users/1# 全量更新 PATCH/users/1# 部分更新 DELETE/users/1# 删除 大白话URL 里只放东西动作用 HTTP 方法表达。2.命名规范 ┌───────────┬──────────────────────────────┬──────────────────────────┐ │ 项目 │ 规则 │ 例子 │ ├───────────┼──────────────────────────────┼──────────────────────────┤ │ URL 路径 │ 小写、连字符、复数名词 │/order-items │ ├───────────┼──────────────────────────────┼──────────────────────────┤ │ JSON 字段 │ 小驼峰 或 下划线团队统一 │ userId 或 user_id │ ├───────────┼──────────────────────────────┼──────────────────────────┤ │ 时间格式 │ ISO8601带时区 │2026-05-22T10:00:00Z │ ├───────────┼──────────────────────────────┼──────────────────────────┤ │ 布尔字段 │ is/has 前缀 │ isActive、hasPaid │ ├───────────┼──────────────────────────────┼──────────────────────────┤ │ 集合返回 │ 永远是数组 │ 没数据返回[]不是 null │ └───────────┴──────────────────────────────┴──────────────────────────┘3.状态码用对别全返回2002xx 成功200OK-一般成功201Created-新建成功POST 后204No Content-成功但没内容返回DELETE 后4xx 客户端错你写错了400Bad Request-参数格式错401Unauthorized-没登录403Forbidden-登录了但没权限404Not Found-资源不存在409Conflict-冲突比如重复创建422Unprocessable-参数对但业务规则不通过429Too Many Requests-限流了5xx 服务器错我们写错了500Internal Error-程序崩了502Bad Gateway-下游服务挂了503Service Unavailable-服务过载4.统一的响应结构// 成功{code:0,message:ok,data:{id:1,name:张三},requestId:abc-123}// 失败{code:40001,message:邮箱格式不对,errors:[{field:email,reason:must be valid email}],requestId:abc-124}大白话不管成功失败结构都一样前端写一套处理逻辑就行。requestId 是出事查日志的钥匙。---四、完整代码FastAPI 一套能跑的示范1.项目结构 api-demo/├── main.py # 入口 ├── models.py # 数据模型 ├── schemas.py # 请求/响应结构 ├── deps.py # 鉴权、限流等依赖 ├── errors.py # 统一错误处理 ├── routers/│ └── users.py # 用户接口 └── requirements.txt2.requirements.txt fastapi0.110.0uvicorn0.27.0pydantic2.6.0python-jose3.3.0# JWT slowapi0.1.9# 限流3.schemas.py-定义菜单# 大白话定好进来什么样、出去什么样FastAPI 自动帮你校验 from pydantic import BaseModel,EmailStr,Field from datetime import datetime from typing import Optional,List,Generic,TypeVar # 统一响应外壳 TTypeVar(T)classResponse(BaseModel,Generic[T]):code:int0message:strokdata:Optional[T]None requestId:Optional[str]None # 分页结构 classPage(BaseModel,Generic[T]):items:List[T]total:intpage:intpageSize:int# 用户相关 classUserCreate(BaseModel):name:strField(...,min_length1,max_length50,description用户名)email:EmailStrField(...,description邮箱)age:Optional[int]Field(None,ge0,le150)classUserUpdate(BaseModel):name:Optional[str]Field(None,min_length1,max_length50)age:Optional[int]Field(None,ge0,le150)classUserOut(BaseModel):id:intname:str email:str age:Optional[int]isActive:bool createdAt:datetime4.errors.py-统一错误处理 # 大白话业务错误用自定义异常框架自动转成统一格式 from fastapi import Request from fastapi.responses import JSONResponse from fastapi.exceptions import RequestValidationError import uuid classBizError(Exception):def__init__(self,code:int,message:str,status:int400):self.codecode self.messagemessage self.statusstatus # 常用错误常量 class Errors:USER_NOT_FOUND(40401,用户不存在,404)EMAIL_EXISTS(40901,邮箱已被注册,409)UNAUTHORIZED(40101,请先登录,401)FORBIDDEN(40301,没有权限,403)defregister_handlers(app):app.exception_handler(BizError)async defbiz_handler(req:Request,exc:BizError):returnJSONResponse(status_codeexc.status,content{code:exc.code,message:exc.message,data:None,requestId:req.headers.get(X-Request-Id,str(uuid.uuid4()))})app.exception_handler(RequestValidationError)async defvalidation_handler(req:Request,exc:RequestValidationError):errors[{field:..join(map(str,e[loc][1:])),reason:e[msg]}fore in exc.errors()]returnJSONResponse(status_code400,content{code:40000,message:参数校验失败,errors:errors,requestId:req.headers.get(X-Request-Id,str(uuid.uuid4()))})5.deps.py-鉴权限流 # 大白话每个需要登录的接口前面挂一下Depends(get_current_user)from fastapi import Header,Depends from jose import jwt,JWTError from errors import BizError,Errors SECRETyour-secret-keyasync defget_current_user(authorization:strHeader(None)):ifnot authorization or not authorization.startswith(Bearer ):raiseBizError(*Errors.UNAUTHORIZED)tokenauthorization[7:]try:payloadjwt.decode(token,SECRET,algorithms[HS256])return{id:payload[sub],name:payload.get(name)}except JWTError:raiseBizError(*Errors.UNAUTHORIZED)# 管理员校验基于角色 async defrequire_admin(userDepends(get_current_user)):ifuser.get(role)!admin:raiseBizError(*Errors.FORBIDDEN)returnuser6.routers/users.py-用户接口核心 # 大白话增删改查全套演示分页、过滤、版本号、幂等 from fastapi import APIRouter,Depends,Query,Path,Header from typing import Optional from schemas import UserCreate,UserUpdate,UserOut,Response,Page from errors import BizError,Errors from deps import get_current_user from datetime import datetime routerAPIRouter(prefix/v1/users,tags[users])# 假数据库 DB{}_id0router.get(,response_modelResponse[Page[UserOut]],summary查询用户列表)async deflist_users(page:intQuery(1,ge1,description第几页),pageSize:intQuery(20,ge1,le100,description每页多少条),keyword:Optional[str]Query(None,description模糊搜索用户名),isActive:Optional[bool]Query(None),):大白话分页查列表支持搜索和过滤itemslist(DB.values())ifkeyword:items[uforu in itemsifkeyword in u[name]]ifisActive is not None:items[uforu in itemsifu[isActive]isActive]totallen(items)start(page-1)*pageSize itemsitems[start:startpageSize]returnResponse(dataPage(itemsitems,totaltotal,pagepage,pageSizepageSize))router.get(/{userId},response_modelResponse[UserOut],summary查询单个用户)async defget_user(userId:intPath(...,gt0)):userDB.get(userId)ifnot user:raiseBizError(*Errors.USER_NOT_FOUND)returnResponse(datauser)router.post(,response_modelResponse[UserOut],status_code201,summary创建用户)async defcreate_user(body:UserCreate,idempotencyKey:Optional[str]Header(None,aliasIdempotency-Key),): 大白话-邮箱重复要返回409-支持幂等键同一个 key 重复请求只创建一次防止网络重试创建多个 # 幂等检查生产环境用 Redis 存ifidempotencyKey and idempotencyKey in IDEMPOTENT_CACHE:returnResponse(dataIDEMPOTENT_CACHE[idempotencyKey])ifany(u[email]body.emailforu in DB.values()):raiseBizError(*Errors.EMAIL_EXISTS)global _id _id1user{id:_id,name:body.name,email:body.email,age:body.age,isActive:True,createdAt:datetime.utcnow(),}DB[_id]userifidempotencyKey:IDEMPOTENT_CACHE[idempotencyKey]userreturnResponse(datauser)router.patch(/{userId},response_modelResponse[UserOut],summary更新用户)async defupdate_user(userId:int,body:UserUpdate,currentDepends(get_current_user),# 需要登录):userDB.get(userId)ifnot user:raiseBizError(*Errors.USER_NOT_FOUND)# 只能改自己除非管理员ifcurrent[id]!userId and current.get(role)!admin:raiseBizError(*Errors.FORBIDDEN)update_databody.model_dump(exclude_unsetTrue)# 只更新传了的字段 user.update(update_data)returnResponse(datauser)router.delete(/{userId},status_code204,summary删除用户)async defdelete_user(userId:int,currentDepends(get_current_user)):ifuserId not in DB:raiseBizError(*Errors.USER_NOT_FOUND)del DB[userId]#204不返回内容 IDEMPOTENT_CACHE{}7.main.py-入口含中间件、限流、CORS # 大白话把所有零件组装起来 from fastapi import FastAPI,Request from fastapi.middleware.cors import CORSMiddleware from slowapi import Limiter,_rate_limit_exceeded_handler from slowapi.util import get_remote_address from slowapi.errors import RateLimitExceeded import uuid,time,logging from routers import users from errors import register_handlers # 限流器默认每分钟60次 limiterLimiter(key_funcget_remote_address,default_limits[60/minute])appFastAPI(titleDemo API,version1.0.0,description演示 API 设计哲学,docs_url/docs,# Swagger UI redoc_url/redoc,# ReDoc openapi_url/openapi.json)#CORS允许前端跨域调用app.add_middleware(CORSMiddleware,allow_origins[https://yourdomain.com],# 别用* allow_methods[GET,POST,PUT,PATCH,DELETE],allow_headers[*],allow_credentialsTrue,)# 限流 app.state.limiterlimiter app.add_exception_handler(RateLimitExceeded,_rate_limit_exceeded_handler)# 统一错误处理register_handlers(app)# 请求日志RequestId 中间件 app.middleware(http)async defadd_request_id(request:Request,call_next):ridrequest.headers.get(X-Request-Id,str(uuid.uuid4()))starttime.time()responseawaitcall_next(request)cost(time.time()-start)*1000response.headers[X-Request-Id]rid logging.info(f{request.method} {request.url.path} {response.status_code} {cost:.1f}ms rid{rid})returnresponse # 健康检查运维用 app.get(/health,tags[meta])async defhealth():return{status:ok}# 挂接业务路由 app.include_router(users.router)# 启动uvicorn main:app--reload8.运行测试 pip install-r requirements.txt uvicorn main:app--reload # 浏览器打开#http://localhost:8000/docs ←自动生成的交互式文档#http://localhost:8000/redoc ←另一种文档样式# 测试创建带幂等键 curl-X POST http://localhost:8000/v1/users \ -H Content-Type: application/json \ -H Idempotency-Key: abc-001 \ -d {name:张三,email:zhanga.com,age:25}# 测试分页 curlhttp://localhost:8000/v1/users?page1pageSize10keyword张---五、高级话题必须懂1.版本管理 方式1:URL 路径/v1/users ←推荐最直观 方式2:请求头 Accept:application/vnd.myapi.v2json 方式3:查询参数/users?version2大白话API 改大了就出 v2老 v1 至少留6个月让客户慢慢迁移。2.幂等性防重要-GET/PUT/DELETE 天然幂等多次调用结果一样-POST 不幂等多次调用会创建多个→用 Idempotency-Key 解决3.分页方式选择 ┌──────────┬──────────────────┬──────────────────────┐ │ 方式 │ 适用 │ 例子 │ ├──────────┼──────────────────┼──────────────────────┤ │ 页码分页 │ 小数据集、要跳页 │?page1size20│ ├──────────┼──────────────────┼──────────────────────┤ │ 游标分页 │ 大数据、信息流 │?cursorabclimit20│ └──────────┴──────────────────┴──────────────────────┘ 游标分页性能好深翻页不会越来越慢。4.字段筛选节省带宽 GET/users/1?fieldsid,name,email 让前端按需取字段别一次把整个对象都返回。5.HATEOAS返回里带下一步链接{data:{id:1,name:张三},links:{self:/v1/users/1,orders:/v1/users/1/orders,delete:/v1/users/1}}大白话前端不用拼 URL跟着链接走就行。6.安全清单-✅ HTTPS 强制-✅ 鉴权JWT/OAuth2-✅ 限流按 IP/用户/API Key-✅ 参数校验永远不信任输入-✅ SQL 注入/XSS 防护-✅ 敏感字段脱敏返回手机号138****1234-✅ 日志不打印 token/密码---六、大白话总结 ┌──────┬──────────────────────────────┬───────────────────────────┐ │ 阶段 │ 关键动作 │ 工具 │ ├──────┼──────────────────────────────┼───────────────────────────┤ │ 设计 │ 先写 OpenAPI 文档再写代码 │ Swagger Editor、Apifox │ ├──────┼──────────────────────────────┼───────────────────────────┤ │ 编码 │ 参数校验、统一响应、统一错误 │ FastAPI/Spring/NestJS │ ├──────┼──────────────────────────────┼───────────────────────────┤ │ 测试 │ 自动化测试Mock │ Postman、pytest │ ├──────┼──────────────────────────────┼───────────────────────────┤ │ 文档 │ 自动从代码生成别手写 │ Swagger UI/ReDoc │ ├──────┼──────────────────────────────┼───────────────────────────┤ │ 发布 │ 灰度版本号监控 │ K8sPrometheus │ ├──────┼──────────────────────────────┼───────────────────────────┤ │ 维护 │ 老版本至少保留半年 │ API Gateway 做路由 │ └──────┴──────────────────────────────┴───────────────────────────┘ 一句话哲学好 API 就像好的菜单——名字清楚、价格明白、点错了告诉你怎么改、加新菜不影响老菜。先想清楚再写代码文档先 行 统一格式、统一错误、统一鉴权改版要慢、要兼容、要监控。 把上面这套代码跑起来打开/docs你就有了一个工业级的 API 框架往里加业务就行。