学习笔记:RESTful API 最佳实践 📝
嘿,大家好👋!这篇文档并不是一个教程,而是我用来辅助自己学习和个人复习的笔记。如果你对完整的内容感兴趣,并且希望通过系统学习来掌握它,那么请直接访问原文章🔗 进行学习吧!
API 设计
一套优秀的 API 设计,需要具备如下特性:
- 使用 HTTPS 🔒
- 使用域名 🏢
- 版本区分 🔢
- 使用 URL 来定位资源 🌐
- 支持资源过滤 🔍
- 数据响应的一致性 🔄
- 支持限流 ⛑️
- API 文档 📚
- 自带分页链接 🔗
- 强制 User-Agent 🤖
1. 使用 HTTPS
- 生产环境,请务必使用 HTTPS
- 注意:非 HTTPS 的 API 调用,不要重定向到 HTTPS,而要直接返回调用错误以禁止不安全的调用。
2. 使用域名
应当尽可能的将 API 与其主域名区分开,使用专用的域名或放在主域名下:
"https://api.gohub.com"
或者:
"https://www.gohub.com/api"
好处:
- 无 Cookie 共享:API 域名不使用主域名的会话机制,免去了无意义的 Cookie 传输,提高了响应速度;
- CDN 加速:高流量时,我们可以对 GET 的 API 请求做 CDN 加速;
- 专属的域名:方便我们做 gzip 以及过期标头的设置。
3. 版本控制
随着业务的发展,API 的迭代是必然的,为了保证开发的顺利进行,我们需要控制好 API 的版本。
通常情况下,有两种做法:
- 将版本号直接加入 URL 中:
"https://api.gohub.com/v1"
"https://api.gohub.com/v2"
- 使用 HTTP 请求头的
Accept
字段进行区分(推荐)
Accept: application / prs.gohub.v1 + json;
Accept: application / prs.gohub.v2 + json;
4. 使用 URL 定位资源
在 RESTful 的架构中,每一个 URL 都代表着一种资源,资源应当是一个名词,而且大部分情况下是名词的复数。
错误的例子:
POST https://api.gohub.com/createTopic
GET https://api.gohub.com/topic/show/1
POST https://api.gohub.com/topics/1/comments/create
POST https://api.gohub.com/topics/1/comments/100/deleteCopy
正确的例子:
POST https://api.gohub.com/topics
GET https://api.gohub.com/topics/1
POST https://api.gohub.com/topics/1/comments
DELETE https://api.gohub.com/topics/1/comments/100
习惯:阅读 URL 时连带 HTTP 方法,把 HTTP 方法当成动词:
- GET —— 获取
- POST —— 创建
- PUT/PATCH —— 更新
- DELETE —— 删除
5. 资源过滤
?page=2&per_page=100:访问第几页数据,每页多少条。
?sort=name&order=asc:指定返回结果按照哪个属性排序,以及排序顺序。
API 端也应当对过滤的参数做验证。例如 per_page
只允许 10~100 的区间,或者 order
的值只能是 asc
或 desc
。
6. 数据响应格式
我们将使用 JSON 作为主要的数据响应格式。
程序也应当在框架上支持不同的响应格式。
一般通过解析 Accept
标头来辨认需要的格式:
//api.gohub.com/
https: Accept: application / prs.gohub.v1 + json;
Accept: application / prs.gohub.v1 + xml;
对于错误数据,默认使用如下结构:
{
"message": "请求验证失败,请检查",
"errors": {
"name": [
"姓名 必须为字符串。"
]
},
"error_code": 10201
}
7. 支持限流
为了防止服务器被攻击,减少服务器压力,需要对接口进行合适的限流控制。
限流时,需要在响应头信息中加入合适的信息,告知客户端当前的限流情况:
X-RateLimit-Limit :10000 //最大访问次数
X-RateLimit-Remaining :9993 //剩余的访问次数
X-RateLimit-Reset :1513784506 //到该时间点,访问次数会重置为 X-RateLimit-Limit
超过限流次数后,需要返回 429 Too Many Requests
状态码。
8. API 文档
我们需要提供清晰的文档,尽可能包括以下几点:
- 包括每个接口的请求参数,每个参数的类型限制,是否必填,可选的值等。
- 响应结果的例子说明,包括响应结果中,每个参数的释义。
- 对于某一类接口,需要有尽量详细的文字说明,比如针对一些特定场景,接口应该如何调用。
- 错误码要做详细标记。
9. 自带分页链接
{
"data": [
{
"id": 1,
.
.
.
"updated_at": "2021-12-26T22:54:26.38+08:00"
},
{
"id": 2,
.
.
.
"updated_at": "2021-12-26T22:54:26.38+08:00"
}
],
"pager": {
"HasPages": true,
"CurrentPage": 2,
"PerPage": 2,
"TotalPage": 54,
"TotalCount": 108,
"NextPageURL": "https://api.gohub.com/v1/topics?page=3&sort=id&order=asc&per_page=2",
"PrevPageURL": "https://api.gohub.comv1/topics?page=1&sort=id&order=asc&per_page=2"
}
}
把分页逻辑控制在服务端,既免去了客户端的 URL 拼接,方便调用,另一方面增加了灵活度。
后续产品需求中,我们需要新增默认过滤规则,客户端的调用不需要修改。
10. 强制 User-Agent
强制客户端在请求时,发送 User-Agent
信息(包含两部分,客户端信息 + 版本)。
User-Agent: Gohub iOS/2.1.37
User-Agent: Gohub Android/2.1.22
收到 User-Agent
数据后可以暂时不做处理,但是后续有特殊的业务需求时,可以针对某个客户端具体到版本,进行特殊的数据处理。
针对此情况,可通过后端 API 判断 User-Agent 标头,对低于 5.0 的版本的客户端请求,返回专属的数据,如 APP 首页的第一个 Banner 显示请升级客户端,安全升级无法使用的提示。
附:程序结构:
.├── app // 程序具体逻辑代码
│ ├── cmd // 命令
│ │ ├── cache.go
│ │ ├── cmd.go
│ │ ├── key.go
│ │ ├── make // make 命令及子命令
│ │ │ ├── make.go
│ │ │ ├── make_apicontroller.go
│ │ │ ├── make_cmd.go
│ │ │ ├── make_factory.go
│ │ │ ├── make_migration.go
│ │ │ ├── make_model.go
│ │ │ ├── make_policy.go
│ │ │ ├── make_request.go
│ │ │ ├── make_seeder.go
│ │ │ └── stubs // make 命令的模板
│ │ │ ├── apicontroller.stub
│ │ │ ├── cmd.stub
│ │ │ ├── factory.stub
│ │ │ ├── migration.stub
│ │ │ ├── model
│ │ │ │ ├── model.stub
│ │ │ │ ├── model_hooks.stub
│ │ │ │ └── model_util.stub
│ │ │ ├── policy.stub
│ │ │ ├── request.stub
│ │ │ └── seeder.stub
│ │ ├── migrate.go
│ │ ├── play.go
│ │ ├── seed.go
│ │ └── serve.go
│ ├── http // http 请求处理逻辑
│ │ ├── controllers // 控制器,存放 API 和视图控制器
│ │ │ ├── api // API 控制器,支持多版本的 API 控制器
│ │ │ │ └── v1 // v1 版本的 API 控制器
│ │ │ │ ├── users_controller.go
│ │ │ │ └── ...
│ │ └── middlewares // 中间件
│ │ ├── auth_jwt.go
│ │ ├── guest_jwt.go
│ │ ├── limit.go
│ │ ├── logger.go
│ │ └── recovery.go
│ ├── models // 数据模型
│ │ ├── user // 单独的模型目录
│ │ │ ├── user_hooks.go // 模型钩子文件
│ │ │ ├── user_model.go // 模型主文件
│ │ │ └── user_util.go // 模型辅助方法
│ │ └── ...
│ ├── policies // 授权策略目录
│ │ ├── category_policy.go
│ │ └── ...
│ └── requests // 请求验证目录(支持表单、标头、Raw JSON、URL Query)
│ ├── validators // 自定的验证规则
│ │ ├── custom_rules.go
│ │ └── custom_validators.go
│ ├── user_request.go
│ └── ...
├── bootstrap // 程序模块初始化目录
│ ├── app.go
│ ├── cache.go
│ ├── database.go
│ ├── logger.go
│ ├── redis.go
│ └── route.go
├── config // 配置信息目录
│ ├── app.go
│ ├── captcha.go
│ ├── config.go
│ ├── database.go
│ ├── jwt.go
│ ├── log.go
│ ├── mail.go
│ ├── pagination.go
│ ├── redis.go
│ ├── sms.go
│ └── verifycode.go
├── database // 数据库相关目录
│ ├── database.db // sqlite 数据文件(加入到 .gitignore 中)
│ ├── factories // 模型工厂目录
│ │ ├── user_factory.go
│ │ └── ...
│ ├── migrations // 数据库迁移目录
│ │ ├── 2021_12_21_102259_create_users_table.go
│ │ ├── 2021_12_21_102340_create_categories_table.go
│ │ └── ...
│ └── seeders // 数据库填充目录
│ ├── users_seeder.go
│ ├── ...
├── pkg // 内置辅助包
│ ├── app
│ ├── auth
│ ├── cache
│ ├── captcha
│ ├── config
│ └── ...
├── public // 静态文件存放目录
│ ├── css
│ ├── js
│ └── uploads // 用户上传文件目录
│ └── avatars // 用户上传头像目录
├── routes // 路由
│ ├── api.go
│ └── web.go
├── storage // 内部存储目录
│ ├── app
│ └── logs // 日志存储目录
│ ├── 2021-12-28.log
│ ├── 2021-12-29.log
│ ├── 2021-12-30.log
│ └── logs.log
└── tmp // air 的工作目录
├── .env // 环境变量文件
├── .env.example // 环境变量示例文件
├── .gitignore // git 配置文件
├── .air.toml // air 配置文件
├── .editorconfig // editorconfig 配置文件
├── go.mod // Go Module 依赖配置文件
├── go.sum // Go Module 模块版本锁定文件
├── main.go // Gohub 程序主入口
├── Makefile // 自动化命令文件
├── readme.md // 项目 readme
.gitignore
tmp
.env
gohub
.DS_Store
.history
# Golang #
######################
# `go test -c` 生成的二进制文件
*.test
# go coverage 工具
*.out
*.prof
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
# 编译文件 #
###################
*.com
*.class
*.dll
*.exe
*.o
*.so
# 压缩包 #
############
# Git 自带压缩,如果这些压缩包里有代码,建议解压后 commit
*.7z
*.dmg
*.gz
*.iso
*.jar
*.rar
*.tar
*.zip
# 日志文件和数据库 #
######################
*.log
*.sqlite
*.db
# 临时文件 #
######################
tmp/
.tmp/
# 系统生成文件 #
######################
.DS_Store
.DS_Store?
.AppleDouble
.LSOverride
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
.TemporaryItems
.fseventsd
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# IDE 和编辑器 #
######################
.idea/
/go_build_*
out/
.vscode/
.vscode/settings.json
*.sublime*
__debug_bin
.project
# 前端工具链 #
######################
.sass-cache/*
node_modules/
热重载工具:air
air 配置信息:
我们可以使用 .air.toml
文件来配置 air
的行为。下面的配置项 include_ext
里,加了两个后缀:env
和 gohtml
,后面的项目中我们会用到。
.air.toml:
# https://github.com/cosmtrek/air/blob/master/air_example.toml TOML 格式的配置文件
# 工作目录
# 使用 . 或绝对路径,请注意 `tmp_dir` 目录必须在 `root` 目录下
root = "."
tmp_dir = "tmp"
[build]
# 由`cmd`命令得到的二进制文件名
# Windows平台示例:bin = "./tmp/main.exe"
bin = "./tmp/main"
# 只需要写你平常编译使用的shell命令。你也可以使用 `make`
# Windows平台示例: cmd = "go build -o ./tmp/main.exe ."
cmd = "go build -o ./tmp/main ."
# 如果文件更改过于频繁,则没有必要在每次更改时都触发构建。可以设置触发构建的延迟时间
delay = 1000
# 忽略这些文件扩展名或目录
exclude_dir = ["assets", "tmp", "vendor","public/uploads"]
# 忽略以下文件
exclude_file = []
# 使用正则表达式进行忽略文件设置
exclude_regex = []
# 忽略未变更的文件
exclude_unchanged = false
# 监控系统链接的目录
follow_symlink = false
# 自定义参数,可以添加额外的编译标识,例如添加 GIN_MODE=release
full_bin = ""
# 监听以下指定目录的文件
include_dir = []
# 监听以下文件扩展名的文件.
include_ext = ["go", "tpl", "tmpl", "html", "gohtml", "env"]
# kill 命令延迟
kill_delay = "0s"
# air的日志文件名,该日志文件放置在你的`tmp_dir`中
log = "air.log"
# 在 kill 之前发送系统中断信号,windows 不支持此功能
send_interrupt = false
# error 发生时结束运行
stop_on_error = true
# 命令附加参数 (bin/full_bin). Will run './tmp/main hello world'.
args_bin = []
[color]
# 自定义每个部分显示的颜色。如果找不到颜色,使用原始的应用程序日志。
main = "magenta"
watcher = "cyan"
build = "yellow"
runner = "green"
[log]
# 显示日志时间
time = false
[misc]
# 退出时删除tmp目录
clean_on_exit = false