作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Alexander Zinchuk的头像

Alexander Zinchuk

Alex十多年的JS编程经验教会了他这种语言的内部原理. 他领导了Yandex的开发团队,并构建了容错系统.

Expertise

Years of Experience

18

Share

大多数前端和后端开发人员以前都处理过REST规范和RESTful api. 但并非所有RESTful api都是一样的. 事实上,它们根本就没有RESTful…

What Is a RESTful API?

It’s a myth.

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

一方面,REST包含了太多模糊和模棱两可的定义. For example, in practice, HTTP方法和状态码字典中的一些术语的使用与其预期目的相反, or not used at all.

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

But wait, it’s not that bad!

你需要REST API规范做什么?

尽管存在这些缺点,但使用合理的方法,REST仍然是一个令人惊叹的概念 creating really great APIs. 这些api可以是一致的,并且具有清晰的结构, good documentation, and high unit test coverage. 你可以用一个高质量的 API specification.

通常一个REST API规范与它的 documentation. 例如,与规范(api的正式描述)不同,文档是人类可读的, 由使用API的移动或web应用程序的开发人员阅读.

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

  • 让你的单元测试更简单、更可靠;
  • 设置用户输入的预处理和验证;
  • Automate serialization and ensure response consistency; and even
  • 享受静态类型的好处.

但首先,让我们从API规范世界的介绍开始.

OpenAPI

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

  1. 带有API名称、描述和版本以及任何附加信息的标头.
  2. 所有资源的描述, including identifiers, HTTP methods, all input parameters, response codes, and body data types, with links to definitions.
  3. 所有可用于输入或输出的定义, in JSON Schema format (which, yes, 也可以用YAML表示吗.)

OpenAPI的结构有两个明显的缺点:过于复杂,有时冗余. 一个小项目可以有数千行JSON规范. 手动维护该文件变得不可能. 这对在开发API时保持规范最新的想法是一个重大威胁.

有多个编辑器允许您描述API并生成OpenAPI输出. 基于它们的其他服务和云解决方案包括Swagger, Apiary, Stoplight, Restlet, and many others.

However, 这些服务对我来说很不方便,因为需要快速编辑规范并将其与代码更改对齐. 此外,功能列表依赖于特定的服务. For example, 基于云服务的工具创建成熟的单元测试几乎是不可能的. 代码生成和模拟端点, while seeming to be practical, 在实践中基本上是没用的. 这主要是因为端点行为通常取决于用户权限和输入参数等各种因素, 这对API架构师来说可能是显而易见的,但不容易从OpenAPI规范中自动生成.

Tinyspec

在本文中,我将使用基于我自己的REST API定义格式的示例, tinyspec. 定义由具有直观语法的小文件组成. 它们描述了项目中使用的端点和数据模型. 文件存储在代码旁边, 提供快速参考,并在编写代码时进行编辑. Tinyspec被自动编译成一个成熟的OpenAPI格式,可以立即在您的项目中使用.

I will also use Node.js (Koa, Express)和Ruby on Rails示例, 但我将演示的实践适用于大多数技术, including Python, PHP, and Java.

Where API Specification Rocks

现在我们有了一些背景知识,我们可以探索如何充分利用适当指定的API.

1. Endpoint Unit Tests

行为驱动开发(BDD)是开发REST api的理想选择. 最好不要为单独的类编写单元测试, models, or controllers, but for particular endpoints. 在每个测试中,模拟一个真实的HTTP请求并验证服务器的响应. For Node.js there are the supertest and chai-http 用于模拟请求的包,对于Ruby on Rails有 airborne.

Let’s say we have a User schema and a 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 () => {
    {状态,主体:{users}} =请求.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]);
  });
});

Ruby on Rails

describe 'GET /users' do
  it 'List all users' do
    get '/users'

    expect_status(200)
    expect_json_types('users.*', {
      name: :string,
      isAdmin: :boolean,
      age: :integer_or_null,
    })
  end
end

当我们已经有了描述服务器响应的规范时, 我们可以简化测试,只检查响应是否符合规范. We can use tinyspec models, 每个都可以转换成遵循JSON Schema格式的OpenAPI规范.

Any literal object in JS (or Hash in Ruby, dict in Python, associative array in PHP, and even Map (在Java中)可以验证是否符合JSON模式. 例如,甚至有适合测试框架的插件 jest-ajv (npm), chai-ajv-json-schema (npm), and json_matchers for RSpec (rubygem).

在使用模式之前,让我们将它们导入到项目中. First, generate the openapi.json 基于tinyspec规范的文件(您可以在每次测试运行之前自动执行此操作):

tinyspec -j -o openapi.json

Node.js

现在您可以在项目中使用生成的JSON并获取 definitions key from it. 该键包含所有JSON模式. 模式可能包含交叉引用($ref),因此如果您有任何嵌入式模式(例如, Blog {posts: Post[]}),则需要将其展开,以便在验证中使用. For this, we will use json-schema-deref-sync (npm).

从'json-schema- def -sync'导入def;
const spec = require('./openapi.json');
const schemas = deref(spec).definitions;

describe('/users', () => {
  it('List all users', async () => {
    {状态,主体:{users}} =请求.get('/users');

    expect(status).to.equal(200);
    // Chai
    expect(users[0]).to.be.validWithSchema(schemas.User);
    // Jest
    expect(users[0]).toMatchSchema(schemas.User);
  });
});

Ruby on Rails

The json_matchers module knows how to handle $ref 引用,但需要在指定位置中单独的模式文件,因此您需要 split the swagger.json 首先将文件分成多个较小的文件:

# ./spec/support/json_schemas.rb
require 'json'
require 'json_matchers/rspec'

JsonMatchers.schema_root = 'spec/schemas'

修复json_matchers的单文件限制
file = File.read 'spec/schemas/openapi.json'
swagger = JSON.解析(file, symbolize_names: true)
swagger[:definitions].keys.each do |key|
  File.open("spec/schemas/#{key}.json", 'w') do |f|
    f.write(JSON.pretty_generate({
      '$ref': "swagger.json#/definitions/#{key}"
    }))
  end
end

测试将是这样的:

describe 'GET /users' do
  it 'List all users' do
    get '/users'

    expect_status(200)
    expect(result[:users][0]).to match_json_schema('User')
  end
end

以这种方式编写测试非常方便. 如果您的IDE支持运行测试和调试(例如, WebStorm, RubyMine, and Visual Studio). This way you can avoid using other software,整个API开发周期被限制为三个步骤:

  1. 在tinyspec文件中设计规范.
  2. 为添加/编辑的端点编写完整的测试集.
  3. 实现满足测试的代码.

2. Validating Input Data

OpenAPI不仅描述了响应格式,还描述了输入数据. 这允许您在运行时验证用户发送的数据,并确保数据的一致性 secure database updates.

假设我们有以下规范, 它描述了用户记录的补丁和允许更新的所有可用字段:

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

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

Previously, 我们探索了用于测试验证的插件, but for more general cases, there are the ajv (npm) and json-schema (rubygem) validation modules. 让我们用它们来写一个带有验证的控制器:

Node.js (Koa)

This is an example for Koa, Express的继承者——但是等价的Express代码看起来很相似.

从'koa-router'中导入路由器;
import Ajv from 'ajv';
import { schemas } from './schemas';

const router = new Router();

// Koa中的标准资源更新操作.
router.patch('/:id', async (ctx) => {
  const updateData = ctx.body.user;

  //使用API规范中的JSON模式进行验证.
  await validate(schemas.UserUpdate, updateData);

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

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

异步函数validate(schema, data) {
  const ajv = new Ajv();

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

在本例中,服务器返回a 500 Internal Server Error 如果输入与规范不匹配,则响应. To avoid this, 我们可以捕获验证器错误并形成自己的答案,该答案将包含有关验证失败的特定字段的更详细信息, and follow the specification.

的定义 FieldsValidationError:

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

InvalidField {name, message}

FieldsValidationError < Error {fields: InvalidField[]}

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

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

此方法允许您编写单元测试,以便在来自客户端的无效数据时测试错误场景的正确性.

3. Model Serialization

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

在响应中为这些实体形成JSON表示的过程被调用 serialization.

有许多用于序列化的插件:例如, sequelize-to-json (npm), acts_as_api (rubygem), and jsonapi-rails (rubygem). Basically, 这些插件允许您为必须包含在JSON对象中的特定模型提供字段列表, as well as additional rules. 例如,您可以重命名字段并动态计算它们的值.

当一个模型需要几种不同的JSON表示时,就更难了, 或者当对象包含嵌套实体-关联时. 然后,您开始需要继承、重用和序列化器链接等特性.

不同的模块提供不同的解决方案, 但让我们考虑一下:规范能否再次提供帮助? 基本上所有关于JSON表示需求的信息, 所有可能的字段组合, including embedded entities, are already in it. 这意味着我们可以编写一个自动序列化器.

Let me present the small sequelize-serialize (npm)模块,该模块支持对Sequelize模型执行此操作. 它接受一个模型实例或一个数组, and the required schema, 然后遍历它来构建序列化的对象. 它还说明了所有必需的字段,并为它们的关联实体使用嵌套模式.

So, 假设我们需要返回博客中有帖子的所有用户, 包括对这些帖子的评论, from the API. 让我们用以下规范来描述它:

# models.tinyspec
注释{authorId: i, message}
Post{主题、消息、评论?: Comment[]}
User {name, isAdmin: b, age?: i}
UserWithPosts < User {posts: Post[]}

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

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

从'koa-router'中导入路由器;
从' sequize -serialize'导入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(用户,模式).UserWithPosts);
});

这简直太神奇了,不是吗?

4. Static Typing

如果你足够酷,可以使用TypeScript或Flow, you might have already asked, “那我珍贵的静态类型呢?!” With the sw2dts or swagger-to-flowtype 模块,您可以基于JSON模式生成所有必要的静态类型,并在测试中使用它们, controllers, and serializers.

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;

  // Run spec validation
  await validate(schemas.UserUpdate, userData);

  // Query the database
  const user = await User.findById(ctx.params.id);
  await user.update(userData);

  // Return serialized result
  const serialized: Api.User = serialize(User, schema).User);
  ctx.body = { user: serialized };
});

And tests:

it('Update user', async () => {
  //测试输入数据的静态检查.
  const updateData: Api.UserUpdate = {name: MODIFIED};

  const res = await request.patch('/users/1', {user: updateData});

  //为请求响应输入helper:
  const user: Api.User = res.body.user;

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

注意,生成的类型定义不仅可以在API项目中使用, 还可以在客户端应用程序项目中描述与API一起工作的函数的类型. (Angular开发者会对此特别高兴的.)

5. Casting Query String Types

如果您的API出于某种原因使用 应用程序/ x-www-form-urlencoded MIME type instead of application/json,请求体看起来像这样:

param1=value&param2=777&param3=false

查询参数也是如此(例如,在 GET requests). 在这种情况下,web服务器将无法自动识别类型:所有数据 will be in string format,所以在解析之后,你会得到这个对象:

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

In this case, 该请求将无法通过模式验证, 因此,您需要手动验证正确的参数格式,并将其转换为正确的类型.

正如您所猜到的,您可以使用规范中的老模式来实现. 假设我们有这个端点和下面的模式:

# posts.endpoints.tinyspec
GET /posts?PostsQuery

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

下面是对这个端点的请求:

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

Let’s write the castQuery 函数将所有参数强制转换为所需类型:

castQuery(查询,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;
    }
 });
}

更完整的实现,支持嵌套模式、数组和 null types is available in the cast-with-schema (npm) module. Now let’s use it in our code:

router.get('/posts', async (ctx) => {
  //将参数强制转换为期望的类型
  const query = castQuery(ctx.query, schemas.PostsQuery);

  // Run spec validation
  await validate(schemas.PostsQuery, query);

  // Query the database
  const posts = await Post.search(query);

  // Return serialized result
  ctx.Body = {posts:序列化(posts, schema).Post) };
});

注意,四行代码中有三行使用了规范模式.

Best Practices

这里有许多我们可以遵循的最佳实践.

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

通常,描述服务器响应的模式与描述用于创建和编辑模型的输入的模式不同. 中的可用字段列表 POST and PATCH 请求必须严格限制,并且 PATCH 通常将所有字段标记为可选. 描述响应的模式可以更加自由.

When you 自动生成CRUDL端点, tinyspec uses New and Update postfixes. User* 模式可以用以下方式定义:

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

尽量不要对不同的操作类型使用相同的模式,以避免由于重用或继承旧模式而导致意外的安全问题.

遵循模式命名约定

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

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

后缀可以变化和组合. 它们的名称仍然必须反映本质,并使文档更易于阅读.

基于客户端类型分离端点

通常,相同的端点会根据客户端类型返回不同的数据, 或者发送请求的用户的角色. For example, the GET /users and GET /messages 对于移动应用程序用户和后台管理人员来说,端点可能有很大的不同. 端点名称的更改可能是开销.

要多次描述同一端点,可以在路径后面的括号中添加其类型. 这也简化了标记的使用:您可以将端点文档分成不同的组, 每一个都针对特定的API客户端组. For example:

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

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

REST API Documentation Tools

在您获得tinyspec或OpenAPI格式的规范之后, 您可以生成HTML格式的漂亮文档并发布它. 这将使使用您的API的开发人员感到高兴, 而且它肯定比手工填写REST API文档模板要好.

除了前面提到的云服务之外,还有一些CLI工具可以转换OpenAPI 2.0到HTML和PDF,可以部署到任何静态主机. Here are some examples:

Do you have more examples? Share them in the comments.

遗憾的是,尽管OpenAPI 3在一年前就发布了.0的支持仍然很差,我没有找到云解决方案和CLI工具中基于它的适当文档示例. 出于同样的原因,tinyspec不支持OpenAPI 3.0 yet.

Publishing on GitHub

发布文档的最简单方法之一是 GitHub Pages. 只需启用对静态页面的支持 /docs 文件夹,并将HTML文档存储在此文件夹中.

通过GitHub Pages从/docs文件夹中托管REST规范的HTML文档.

可以通过tinyspec或其他CLI工具添加命令来生成文档 scripts/package.json 文件在每次提交后自动更新文档:

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

Continuous Integration

您可以在CI周期中添加文档生成并发布它, for example, 根据环境或API版本(如 /docs/2.0, /docs/stable, and /docs/staging.)

Tinyspec Cloud

如果您喜欢tinyspec语法,那么您可以成为的早期采用者 tinyspec.cloud. 我们计划在此基础上构建一个云服务和一个CLI,用于文档的自动部署,并提供广泛的模板选择和开发个性化模板的能力.

REST规范:一个不可思议的神话

REST API开发可能是现代web和移动服务开发中最令人愉快的过程之一. There are no browser, operating system, and screen-size zoos, 一切都在你的掌控之中, at your fingertips.

由于支持自动化和最新的规范,这个过程变得更加容易. 使用我所描述的方法的API变得结构良好、透明和可靠.

底线是,如果我们要创造一个神话,为什么不让它成为一个了不起的神话呢?

Understanding the basics

  • What is REST?

    REST是一种web服务架构风格,定义了一组必需的约束. 它基于具有唯一标识符(uri)的资源和对所述资源的操作. Additionally, REST规范需要客户机-服务器模型, a uniform interface, 以及服务器存储状态的缺失.

  • 什么是OpenAPI规范?

    OpenAPI规范是一种被普遍接受的描述REST api的格式. 该规范由单个JSON或YAML文件组成,其中包含通用API信息, 所有使用资源的描述, 数据采用JSON Schema格式.

  • What is Swagger?

    Swagger是2016年之前开放API规范的名称. 目前Swagger是一个独立的项目, 提供了许多开源和商业工具以及云服务,用于起草和开发OpenAPI规范. 它还提供了用于服务器代码生成和自动化端点测试的工具.

  • What is JSON Schema?

    JSON模式是JSON对象(文档)的规范。. 它由所有可用属性及其类型的列表组成, 包括所需属性的列表.

  • What exactly is an API?

    应用程序编程接口(API)是软件单元之间通信的一种方式. Usually, 它是一组可用的方法, commands, 以及一个软件组件提供给其他组件的定义.

  • What is an API specification?

    API规范是一种特殊格式的文档,它提供了API方法和特性的清晰定义, 以及它们可能的构型.

  • API文档是什么意思?

    API文档是一种人类可读的文档,用于第三方开发人员学习API特性并构建使用所描述API的自己的软件.

  • What makes a RESTful API?

    这是一个HTTP API,它遵循REST架构风格定义的标准和约束. 然而,在实践中,几乎没有api是100% RESTful的.

  • 行为驱动型发展是什么意思?

    BDD是一种软件开发技术,它意味着每个小程序更改都必须针对预先指定的行为进行测试. 因此,开发人员首先定义预期行为, 通常以自动化单元测试的形式, and then implement the code, 确保所有行为测试都通过了.

  • What is tinyspec?

    Tinyspec是REST API文档的简写,可编译为OpenAPI格式. 它的目的是使规范的设计和开发更容易. For example, 它允许您拆分端点(资源)定义,并将数据模型存储在源代码旁边的单独的较小文件中.

就这一主题咨询作者或专家.
Schedule a call
Alexander Zinchuk的头像
Alexander Zinchuk

Located in Barcelona, Spain

Member since September 15, 2016

About the author

Alex十多年的JS编程经验教会了他这种语言的内部原理. 他领导了Yandex的开发团队,并构建了容错系统.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

Expertise

Years of Experience

18

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

Toptal Developers

Join the Toptal® community.