关于我 壹文 项目 三五好友
学习笔记:RESTful API 最佳实践 📝 2024-09-22
文章摘要

学习笔记:RESTful API 最佳实践 📝

嘿,大家好👋!这篇文档并不是一个教程,而是我用来辅助自己学习和个人复习的笔记。如果你对完整的内容感兴趣,并且希望通过系统学习来掌握它,那么请直接访问原文章🔗 进行学习吧!

API 设计

一套优秀的 API 设计,需要具备如下特性:

  1. 使用 HTTPS 🔒
  2. 使用域名 🏢
  3. 版本区分 🔢
  4. 使用 URL 来定位资源 🌐
  5. 支持资源过滤 🔍
  6. 数据响应的一致性 🔄
  7. 支持限流 ⛑️
  8. API 文档 📚
  9. 自带分页链接 🔗
  10. 强制 User-Agent 🤖

1. 使用 HTTPS

  1. 生产环境,请务必使用 HTTPS
  2. 注意:非 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 的版本。

通常情况下,有两种做法:

  1. 将版本号直接加入 URL 中:
"https://api.gohub.com/v1"
"https://api.gohub.com/v2"
  1. 使用 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 的值只能是 ascdesc

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
Not-By-AI