你确定你的 REST API 真的符合 REST 规范?

news2024/11/27 0:26:35

RESTful API 的存在是 web 开发历史上的一个里程碑。在本文中,我将和你探讨几种节省 REST API 开发时间的方法,并给出相关的 Node.js 示例。

什么是 RESTful API

首先,想问一个问题,你的项目里真的有真正的 RESTful API 吗?

如果你认为你的项目有 RESTful API,那么你很可能错了。RESTful API 背后的思想是遵循REST 规范中描述的所有架构规则和限制的方式进行开发。然而,实际上,这在实践中基本上是不可能的。

一方面,REST 包含了太多模糊和模棱两可的定义。例如,在实践中,HTTP 方法和状态码中的一些术语的使用与其预期目的相反,或者根本不使用。

另一方面,REST 开发产生了太多的限制。例如,对于在移动应用程序中使用的实际 api,原子资源的使用是次优的。再如,完全拒绝请求之间的数据存储实质上禁止了随处可见的“用户会话”机制。

不过,我想说,也没你想的那么糟糕!

REST API 规范能做什么?

尽管存在上面说到的缺点,但使用合理的方法,REST 仍然是创建真正优秀 api 的一个绝佳选择。因为你通过高质量的 API 规范实现的 api 将会是一致的,具有清晰的结构、良好的文档和高的单元测试覆盖率。

通常,REST API规范与其文档相关联。

正确的 API 描述不仅仅是编写好 API 文档。在这篇文章中,我想分享一些例子,教你如何做到:

  • 单元测试更简单、更可靠;
  • 用户输入的预处理和验证;
  • 自动序列化,确保响应一致性;
  • 静态类型

但首先,让我们从 API 规范开始。

OpenAPI 规范

OpenAPI 是目前最广泛接受的 REST API 规范格式。该规范以 JSON 或 YAML 格式编写在单个文件中,由三个部分组成:

  1. 带有 API 名称、描述和版本以及任何附加信息的标头。
  2. 所有资源的描述,包括标识符、HTTP 方法、所有输入参数、响应代码和主体数据类型,以及指向定义的链接。
  3. 所有可用于输入或输出的定义,以 JSON 模式格式。

不过,OpenAPI 的结构有两个明显的缺点:过于复杂和冗余。例如,一个小项目就可以产生数千行 JSON 规范。手动维护该文件变得有些难。这对开发者来说是一个威胁。

虽然有许多解决方案,如 Swagger、Apiary、stolight、Restlet。但,这些服务对我来说是不方便的,因为需要快速编辑规范并将其与代码更改对齐。

Tinyspec 规范

在本文中,我将使用基于 tinyspec 规范定义 API。tinyspec 定义的 API 是由具有直观语法的小文件组成,这些文件描述了项目中使用的数据模型。同时,这些文件就放置在代码文件旁边,能够在编写代码的时候提供快速的参考。除此之外,Tinyspec 还能够被自动编译成一个成熟的 OpenAPI 格式,可以立即在项目中使用。

API规范的基石

上面的内容中,我们介绍了一些背景知识。现在,我们可以开始探索如何充分利用它们进行 API 的编写。

1. 为 API 编写单元测试

行为驱动开发(BDD)是开发 REST api 的理想选择。最好不是为单独的类、模型或控制器编写单元测试,而是为特定的 API 编写单元测试。在每个测试中,模拟一个真实的 HTTP 请求并验证服务器的响应。例如,在 Node.js 的单元测试中,用于模拟请求的有 supertestchai-http包。

现在,假设我们有一个 user.models 和一个返回所有 users 的 GET /users 端点。下面我们就用 tinyspec 语法来描述它:

# user.models.tinyspec
User {name, isAdmin: b, age?: i}

# users.endpoints.tinyspec
GET /users
    => {users: User[]}

接着,我们编写相应的测试:

  • Node.js:
describe('/users', () => {
  it('List all users', async () => {
    const { status, body: { users } } = request.get('/users');

    expect(status).to.equal(200);
    expect(users[0].name).to.be('string');
    expect(users[0].isAdmin).to.be('boolean');
    expect(users[0].age).to.be.oneOf(['boolean', null]);
  });
});

上述测试中,我们测试了服务器的返回值,是否遵循规范。不过这一过程似乎有些冗余。为了简化过程,我们可以使用 tinyspec 模型,每个模型都可以转换为遵循 JSON Schema 格式的OpenAPI 规范。

做之前,你首先需要生成 OpenAPI,请执行如下命令:

tinyspec -j -o openapi.json

接着,你可以在项目中使用生成的 JSON 并从中获取定义键。该键包含所有的 JSON 模型。模型可能包含交叉引用($ref),因此,如果你有任何嵌入式的模型(例如,Blog {posts: Post[]}),则需要将它们展开,以便在验证中使用。为此,我们将使用 json-schema-def-sync这个包帮助我。

import deref from 'json-schema-deref-sync';
const spec = require('./openapi.json');
const schemas = deref(spec).definitions;

describe('/users', () => {
  it('List all users', async () => {
    const { status, body: { users } } = request.get('/users');
    expect(status).to.equal(200);
    // Jest
    expect(users[0]).toMatchSchema(schemas.User);
  });
});

如果你的 IDE 支持运行测试和调试,以这种方式编写测试非常方便。这个时候,整个 API 开发周期被限制为三个步骤:

  1. 在 tinyspec 文件中设计规范。
  2. 为 API 编写完整的测试。
  3. 实现满足测试的代码。

2. 验证输入数据

OpenAPI 不仅描述了响应格式,还描述了输入数据。这允许你在运行时验证用户发送的数据是否一致,以及数据库能够安全地进行更新。

假设我们有以下规范,它描述了对用户信息的更新:

# user.models.tinyspec
UserUpdate !{name?, age?: i}

# users.endpoints.tinyspec
PATCH /users/:id {user: UserUpdate}
    => {success: b}

接着,让我们使用 Koa.js 针对这个规范来写一个带有验证的控制器:

import Router from 'koa-router';
import Ajv from 'ajv';
import { schemas } from './schemas';

const router = new Router();

router.patch('/:id', async (ctx) => {
  const updateData = ctx.body.user;

  // 验证
  await validate(schemas.UserUpdate, updateData);

  const user = await User.findById(ctx.params.id);
  await user.update(updateData);

  ctx.body = { success: true };
});

async function validate(schema, data) {
  const ajv = new Ajv();

  if (!ajv.validate(schema, data)) {
    const err = new Error();
    err.errors = ajv.errors;
    throw err;
  }
}

在本例中,如果输入与规范不匹配,服务器将返回 500 Internal server Error 响应。为了避免这种情况,我们可以捕获验证器错误并形成我们自己的返回,该返回将包含有关验证失败的特定字段的更详细信息,并遵循规范。

接着,让我们添加 FieldsValidationError 的定义:

# error.models.tinyspec
Error {error: b, message}

InvalidField {name, message}

FieldsValidationError < Error {fields: InvalidField[]}

现在让我们把它列为可能的 endpoints 响应之一:

# users.endpoints.tinyspec
PATCH /users/:id {user: UserUpdate}
    => 200 {success: b}
    => 422 FieldsValidationError

此时,当客户端发送了无效数据时,我们的便能 catch 到正确的错误信息。

3.模型序列化

几乎所有现代服务器框架都以这样或那样的方式使用对象关系映射(ORM)。这意味着 API 使用的大部分资源是由模型及其实例和集合表示的。

我们把要在响应中发送的这些实体形成 JSON 表示的过程称为序列化。

有许多用于序列化的插件:例如,sequealize-to-json。基本上,这些插件允许你为必须包含在 JSON 对象中的特定模型提供字段列表,以及附加规则。例如,你可以重命名字段并动态计算它们的值。

不过,当一个模型需要几个不同的 JSON 表示,或者当对象包含嵌套的实体关联时,就比较复杂了。这个时候,你可能会开始通过继承、重用和序列化器链接等特性来解决这些问题。

虽然有不同的解决方案,但让我们思考一下: 规范能否再次为这些场景提供帮助?

这个时候,我想向你推荐一个构建序列化的 npm 模块: Sequelize-serialize,它支持对 Sequelize模型执行相应的操作。例如,它接受一个模型实例或一个数组,以及所需的模式,然后遍历它以构建序列化的对象。

因此,假设我们需要从 API 返回博客中有帖子的所有用户,包括对这些帖子的评论。我们就可以通过以下规范描述它:

# models.tinyspec
Comment {authorId: i, message}
Post {topic, message, comments?: Comment[]}
User {name, isAdmin: b, age?: i}
UserWithPosts < User {posts: Post[]}

# blogUsers.endpoints.tinyspec
GET /blog/users
    => {users: UserWithPosts[]}

现在我们可以使用 Sequelize 构建请求,并返回与上述规范完全对应的序列化对象:

import Router from 'koa-router';
import serialize from 'sequelize-serialize';
import { schemas } from './schemas';

const router = new Router();

router.get('/blog/users', async (ctx) => {
  const users = await User.findAll({
    include: [{
      association: User.posts,
      required: true,
      include: [Post.comments]
    }]
  });

  ctx.body = serialize(users, schemas.UserWithPosts);
});

4. 静态类型

你可能会继续发问:“静态类型怎么办?”

我会向你推荐使用 sw2dtsswagger-to-flowtype 模块,你可以基于 JSON 模型生成所有必要的静态类型,并在测试、控制器和序列化器中使用它们:

tinyspec -j

sw2dts ./swagger.json -o Api.d.ts --namespace Api

现在我们可以在控制器中使用类型和测试:

router.patch('/users/:id', async (ctx) => {
  // 指定请求数据的类型
  const userData: Api.UserUpdate = ctx.request.body.user;

  await validate(schemas.UserUpdate, userData);

  // 查询数据库
  const user = await User.findById(ctx.params.id);
  await user.update(userData);

  // 返回序列化结果
  const serialized: Api.User = serialize(user, schemas.User);
  ctx.body = { user: serialized };
});

让我们进行一些测试:

it('Update user', async () => {
  // 静态类型检查
  const updateData: Api.UserUpdate = { name: MODIFIED };
  const res = await request.patch('/users/1', { user: updateData });
  const user: Api.User = res.body.user;

  expect(user).to.be.validWithSchema(schemas.User);
  expect(user).to.containSubset(updateData);
});

请注意,生成的类型定义不仅可以在 API 项目中使用,还可以在客户端应用程序项目中使用,以描述与 API 一起工作的函数中的类型。

5. 强制转换查询字符串类型

如果你的 API 由于某种原因使用 application/x-www-form-urlencoded MIME类型而不是 application/json 来处理请求,请求体将看起来像这样:

param1=value&param2=777&param3=false

在这种情况下,web 服务器将无法自动识别类型,此时所有数据将是字符串格式,所以解析后你将得到这个对象:

{ param1: 'value', param2: '777', param3: 'false' }

在这种情况下,请求将无法通过模型验证,因此你需要手动验证正确的参数格式,并将其转换为正确的类型。

不过我们可以通过规范来验证。假设我们有如下的 endpointsmodels:

# posts.endpoints.tinyspec
GET /posts?PostsQuery

# post.models.tinyspec
PostsQuery {
  search,
  limit: i,
  offset: i,
  filter: {
    isRead: b
  }
}

下面是对这个 endpoints 的请求:

GET /posts?search=needle&offset=10&limit=1&filter[isRead]=true

接着,让我们编写 castQuery 函数,将所有参数强制转换为所需的类型:

function castQuery(query, schema) {
  _.mapValues(query, (value, key) => {
    const { type } = schema.properties[key] || {};
  
    if (!value || !type) {
      return value;
    }
  
    switch (type) {
      case 'integer':
        return parseInt(value, 10);
      case 'number':
        return parseFloat(value);
      case 'boolean':
        return value !== 'false';
      default:
        return value;
    }
 });
}

现在让我们在代码中使用它:

router.get('/posts', async (ctx) => {
  // 转换类型
  const query = castQuery(ctx.query, schemas.PostsQuery);
  // 验证规范
  await validate(schemas.PostsQuery, query);
  // 查询数据库
  const posts = await Post.search(query);
  // 返回序列化结果
  ctx.body = { posts: serialize(posts, schemas.Post) };
});

最佳实践

OK,介绍了以上内容之后,让我们来做一下遵循规范的最佳实践。

使用单独的创建和编辑模型

通常,描述服务器响应的模型与描述用于 NewUpdate 模型的输入的模型不同。例如,POST 和PATCH 请求中可用的字段列表必须严格限制,PATCH 通常将所有字段标记为可选。描述响应的模型可以更加自由。

当你自动生成 CRUDL 端点时,tinyspec 使用 New 和 Update 后缀。用户模型可以通过以下方式定义:

User {id, email, name, isAdmin: b}
UserNew !{email, name}
UserUpdate !{email?, name?}

遵循模型命名约定

对于不同的端点,相同模型的内容可能会有所不同。在模式名称中使用 With*For* 后缀来显示差异和目的。在 tinyspec 中,模型也可以相互继承。例如:

User {name, surname}
UserWithPhotos < User {photos: Photo[]}
UserForAdmin < User {id, email, lastLoginAt: d}

这样的名称能够反映模型的作用,并使文档更易于阅读。

基于客户端类型分离端点

通常,相同的端点会根据客户端类型或发送请求的用户角色返回不同的数据。例如,对于移动应用程序用户和后台管理人员来说,GET /uses 端点可能存在很大的不同。

因此,如果要多次描述同一端点,可以在路径后面的括号中添加其类型。例如:

Mobile app:
    GET /users (mobile)
        => UserForMobile[]

CRM admin panel:
    GET /users (admin)
        => UserForAdmin[]

REST API 文档工具

在获得 tinyspec 或 OpenAP I格式的规范后,可以生成 HTML 格式的美观文档并发布它。这将使使用你的 API 的开发人员感到轻松,并且肯定比手工填写 REST API 文档模板要好。

下面是一些文档工具:

  • Bootprint-openapi (在 tinyspec 中默认使用)
  • swagger2markup-cli (jar,有一个用法示例,将在 tinyspec Cloud 中使用)
  • redoc-cli
  • widdershins

遗憾的是,尽管发布一年了,OpenAPI 3.0 的支持仍然很差,tinyspec 还不支持 OpenAPI 3.0。

在GitHub上发布

发布文档的最简单方法之一是GitHub Page。只需在存储库设置中为 /docs 文件夹启用对静态页面的支持,并将 HTML 文档存储在此文件夹中即可。

image.png

同时,你可以在 scripts/package.json 中添加命令来通过 tinyspec 或其他 CLI 工具生成文档:

"scripts": {
    "docs": "tinyspec -h -o docs/",
    "precommit": "npm run docs"
}

最后

REST API 开发可能是现代 web 开发和移动端开发中最令人愉快的过程之一。因为它的开发过程没有浏览器、操作系统和屏幕大小的限制,一切都完全在你的控制之下。如果你想让你的 REST API 更具规范,不妨试试文中提到的。

点赞、收藏、加关注,我们下篇文章再见~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/877632.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【构造】CF1734 E

Problem - 1734E - Codeforces 题意&#xff1a; 思路&#xff1a; Code&#xff1a; #include <bits/stdc.h>#define int long longusing i64 long long;constexpr int N 1e3 10; constexpr int mod 1e9 7;int n; int a[N], ans[N][N];void solve() {std::cin &g…

动力节点零基础Java学习教程,从入门到进阶,轻松掌握Java开发技能

Java是一门应用非常广泛的编程语言&#xff0c;对于零基础自学Java来说&#xff0c;Java的学习过程可能会有一些困难&#xff0c;但只要掌握了相关的基础知识和技能&#xff0c;不断地实践和总结&#xff0c;就能真正掌握Java编程。 动力节点老杜的Java经典之作&#xff0c;哔站…

Jenkins构建自由风格项目发布jar到服务器

前面的文章有介绍 docker安装jenkins 和 dockerjenkins发布spring项目&#xff1b;这里就不做过多的介绍&#xff0c;直接说明构建步骤。 1、选择构建一个自由风格的项目 2、 选择丢弃旧的构建 3、配置Git信息 4、构建触发器 和 构建环境可以直接跳过 5、直接来到Build Step…

技术广度必备——高并发设计之分布式锁的实现方式

文章目录 问题背景前言实现基于MySQL实现唯一索引乐观锁悲观锁 基于Redis基于Zookeeper原理使用Curator框架实现ZK分布式锁缺点 问题背景 研究有哪几种方案可以实现分布式锁&#xff0c;技术选型的场景下能用到。 前言 本文参考过的文章有分布式锁的几种实现方式方式大致分为3种…

蓝帽杯 取证2022

网站取证 网站取证_1 下载附件 并解压 得到了一个文件以及一个压缩包 解压压缩包 用火绒查病毒 发现后门 打开文件路径之后 发现了一句话木马 解出flag 网站取证_2 让找数据库链接的明文密码 打开www文件找找 查看数据库配置文件/application/database.php&#xff08;CodeI…

Cuda和cuDNN安装

Cuda和cuDNN安装 1Cuda下载与安装1.1查看适合cuda的版本1.2下载cuda toolkit1.3cuda安装步骤1.4配置环境变量1.5验证 2cuDNN下载与安装2.1cuDNN下载2.2cuDNN配置2.3配置环境变量2.4验证 3安装PyTorch-GPU3.1打开Anaconda Prompt![在这里插入图片描述](https://img-blog.csdnimg…

ARM--day2(cpsr、spsr、数据搬移指令、移位操作指令、位运算操作指令、算数运算指令、比较指令、跳转指令)

.text .global _gcd _gcd:mov r0,#9mov r1,#15b loop loop:cmp r0,r1beq stopsubhi r0,r1bhi loopsubcc r1,r0bcc loopstop:b stop.end用for循环实现1~100之间和5050 .text .global _gcd _gcd:mov r0,#0x0mov r1,#0x1mov r2,#0x64b loop loop:cmp r1,r2bhi stopadd r0,r0,r1ad…

卷王特斯拉又全网降价了,卷死车企们

哈喽,大家好,今天媒介盒子小编又来跟大家分享软文推广的干货知识了,本篇分享的主要内容是&#xff1a;特斯无孔不入的营销手段。 1、特斯拉Model Y降价 车企要打架 自2023 年 8 月 14 日起&#xff0c;Model Y 长续航版起售价从 31.39 万元调整为 29.99 万元&#xff0c;Mode…

React+Typescript清理项目环境

上文 创建一个 ReactTypescript 项目 我们创建出了一个 React配合Ts开发的项目环境 那么 本文 我们先将环境清理感觉 方便后续开发 我们先来聊一下React的一个目录结构 跟我们之前开发的React项目还是有一些区别 public 主要是存放一些静态资源文件 例如 html 图片 icon之类的 …

走进 Linux

一、开关机 开机&#xff1a; 开机会启动许多程序。他们在windows叫做“服务”(service),在Linux就叫做“守护进程”&#xff08;daemon&#xff09;开机成功后&#xff0c;它会显示一个文本登录界面&#xff0c; 这个界面就是我们经常看到的登录界面&#xff0c;在这个登录界…

【有奖体验】COS插件体验,四重好礼等你拿!

对象存储 COS 为降低用户接入门槛&#xff0c;集成了多款 COS 插件&#xff0c;开放供用户使用&#xff0c;包含搭建网站、图床、论坛等多个热门业务场景的插件&#xff0c;让使用更便捷&#xff01;对象存储 COS 准备了多重好礼&#xff0c;欢迎广大同学们踊跃体验 COS 插件&a…

U盘安装CentOS7系统出现dracut timeout的解决办法

文章目录 业务场景操作步骤U盘装CentOS7系统确定U盘盘符修改启动命令系统配置 总结 业务场景 我们在某市实施交通信控平台项目&#xff0c;我们申请了一台服务器&#xff0c;用于平台安装由于机房机器只有内网&#xff0c;不连互联网&#xff0c;我们无法安装所需要的软件&…

FreeRTOS(任务通知)

资料来源于硬件家园&#xff1a;资料汇总 - FreeRTOS实时操作系统课程(多任务管理) 目录 一、任务通知的概念 1、概念 2、发送通知给任务的方式 3、任务通知使用限制 二、任务通知的运行机制 三、任务通知的API函数 1、任务通知的数据结构 2、常用的API函数 3、函数x…

Java多线程编程中的线程死锁

Java多线程编程中的线程死锁 ​ 在多线程编程中&#xff0c;线程死锁是一种常见的问题&#xff0c;它发生在两个或多个线程互相等待对方释放资源的情况下&#xff0c;导致程序无法继续执行。本文将介绍线程死锁的概念、产生原因、示例以及如何预防和解决线程死锁问题。 线程死…

LeNet中文翻译

Gradient-Based Learning Applied to Document Recognition 基于梯度的学习应用于文档识别 摘要 使用反向传播算法训练的多层神经网络构成了成功的基于梯度的学习技术的最佳示例。给定适当的网络架构&#xff0c;基于梯度的学习算法可用于合成复杂的决策表面&#xff0c;该决策…

【C语言实战项目】通讯录

一.了解项目功能 在本次实战项目中我们的目标是实现一个通讯录: 该通讯录可以用来存储1000个人的信息 每个人的信息包括&#xff1a;姓名、年龄、性别、住址、电话 通讯录提供功能有&#xff1a; 添加联系人信息删除指定联系人信息查找指定联系人信息修改指定联系人信息显示所有…

miniExcel 生成excel

一、nuget dotnet add package MiniExcel --version 1.31.2 二、新建表及数据 ExampleProducts 三、这里我用了Dapper.Query方法 读取excel public virtual async Task<IActionResult> Anonymous(){try{//using (var connection _dbContext.GetDbConnection())//{//…

请教电路高手帮忙Review一下是否可行?

想要实现STM32 3.3V GPIO 控制5V电源通断&#xff0c;默认状态为&#xff1a;接通。 使用如下电路图有无问题&#xff1f;参数是否需要调整&#xff1f;

机器学习笔记之优化算法(十二)梯度下降法:凸函数VS强凸函数

机器学习笔记之优化算法——梯度下降法&#xff1a;凸函数VS强凸函数 引言凸函数&#xff1a;凸函数的定义与判定条件凸函数的一阶条件凸函数的梯度单调性凸函数的二阶条件 强凸函数强凸函数的定义强凸函数的判定条件强凸函数的一阶条件强凸函数的梯度单调性强突函数的二阶条件…

为博客获取阿里云免费HTTPS证书:简易指南

文章目录 前置条件&#xff1a;步骤1 例如阿里云控制台&#xff0c;选择SSL证书步骤2 申请购买免费证书步骤3 创建证书步骤3.1 证书申请步骤3.2 DNS域名验证 步骤4 等待证书审核成功&#xff0c;下载证书总结 本文分享&#xff0c;如何在阿里云申请免费HTTPS证书 前置条件&…