如何使用 OpenAPI 编写优秀的 REST API

如何使用 OpenAPI 编写优秀的 REST API
原文标题:How to write robust REST API with OpenAPI
原文链接:https://dev.to/yonatankarp/how-to-write-robust-rest-api-with-openapi-2klo
作者:Yonatan Karp-Rudin


在担任后端工程师期间,我开发了一些 REST API。在开发过程中,我发现后端和客户端之间的集成总会遇到各种各样问题。比如 URL 拼写错误、在JSON 中使用驼峰式字段命名、用 string 而不是 integer 去发送 value 等等问题。

2 年前,我和我的团队设计了一个由多个客户端集成的新 API:移动端(Android 和 iPhone)、Web 端和其他后端服务。我们想让集成尽可能简单,并让 API 定义尽可能“健壮”。

在关于 API 设计的头脑风暴期间,我们决定使用 OpenAPI(以前称为 Swagger)。我很早之前就已经知道 Swagger,但只是作为文档工具使用。这一次,我们决定以以前相反的方式去构建 API。我们先定义包含所有端点、请求和响应的规范,然后规范的每个集成商(后端、移动和 Web)都自动生成模型、客户端或控制器并在代码库中使用它们,这可以避免 API 在使用过程中产生错误。

本次设计取得了巨大成功,我们为后面所有新设计的 API 都采用这种设计模式。而且越来越多的团队开始采用 OpenAPI。正因如此,公司已经决定采用 OpenAPI 作为描述我们所有 API 的官方方式,方便公司内外的 API 进行集成。

在本文中,我将展示一个简单的 API 示例以及我们如何设计一个可用的 REST API 的过程。在我们的技术栈中,将使用 SpringBoot 作为框架,使用 Kotlin 作为编程语言,使用 Gradle 的 Kotlin DSL 作为构建系统。需要注意的是,OpenAPI 支持许多不同的语言,而我只是决定使用这其中一种。有关完整列表,你可以查看文档

第 1 步 - 设计 API

我们现在设计一个简单的 API。API 在端点上获取/greet和一个要访问的name作为查询参数(例如/greet&name=Yonatan)。我们用hello + $name作为返回响应。

从定义规范开始,你可以使用 Swagger 编辑器 提前检测规范中的语法错误。如果你使用的是 IntelliJ IDEA,你还可以使用 OpenAPI Editor 插件

这是我定义的规范:

openapi: 3.0.3

info:
  title: Greeting API
  description: "An API that will send you a greet according to a given name"
  contact:
    name: Yonatan Karp-Rudin
    url: https://yonatankarp.com
  version: 0.1.0

tags:
  - name: Greeting

servers:
  - url: http://localhost:8080/v1
    description: Local development environment

paths:
  /greet:
    get:
      operationId: greet_name
      description: Greeting a given name.
      tags:
        - Greeting

      parameters:
        - in: query
          name: name
          schema:
            type: string
          required: false
          description: The name to greet if no name supplied the API will greet the world

      responses:
        200:
          description: Returning a greeting with the given name.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GreetResponse"

components:
  schemas:
    GreetResponse:
      type: object
      properties:
        greet:
          type: string
          description: The greet from the API.
          example: "Hello, Yonatan!"

      required:
        - greet

接下来逐步了解构成规范的每个部分。

API 信息

info部分,我定义了将会出现在文档中的信息以及 API 版本、API 的所有者等。如果你需要添加更多属性,可以在 API 常规信息 中查看。

服务器

服务器列表将允许使用规范文档的开发者选择他们想要使用的环境(例如本地、开发、暂存和生产)。如:

servers:
     - url: https://dev.env
     description: The development environment.
     - url: https://staging.env
     description: The staging environment.
     - url: https://production.env
     description: The production environment.

端点

path部分中,我们定义好即将成为我们 API 一部分的所有不同端点,包括它们的 HTTP 方法、请求和响应。

模型

components部分中定义了不同的模型,这些模型将成为API 的一部分。在我的例子中,只是为端点定义一个响应,这个响应将包含单个字段(该字段不能为null,因为它被标记为 required)。此部分可以包含更多信息,例如安全模式(我们在 API 中使用哪种身份验证方法、所需的通用 headers 等)

第 2 步 - 生成模型

为了生成我们的模型,我们将使用 OpenAPI Generator,更具体地说,我们将使用 OpenAPI Generator Gradle plugin

首先,让我们将规范存储在我们的项目中,我们可以在项目根目录下创建一个api目录,并将spec.yml文件存储其中。项目应如下所示:

下一步是将一个config.json文件添加到我们的api目录,这个文件将包含我们想要设置的不同标志。代码如下:

{
  "interfaceOnly": true,
  "modelPackage": "com.yonatankarp.openapi.models",
  "apiPackage": "com.yonatankarp.openapi",
  "implicitHeaders": true,
  "hideGenerationTimestamp": true,
  "useTags": true
}

你可以在文档中找到其他可用的标志。你的项目结构现在应该如下所示:

现在将插件添加到项目中。打开build.gradle.kt并将以下代码添加到插件中:

plugins {
        id("org.openapi.generator") version "5.3.0"
}

最后配置 Gradle 插件来处理设置的所有内容。

在我们每次创建项目时,将build插入到构建目录中,这样我们就不需要向项目提交任何自动生成的代码。

val apiDirectoryPath = projectDir.absolutePath + File.separator + "api"
val generatedCodeDirectoryPath = buildDir.path + File.separator +
       "generated" + File.separator + "open-api"

openApiGenerate {
    generatorName.set("kotlin-spring")
    inputSpec.set(apiDirectoryPath + File.separator + "spec.yml")
    outputDir.set(generatedCodeDirectoryPath)
    configFile.set(apiDirectoryPath + File.separator + "config.json")
}

注意,如果你需要从生成的类中添加或删除某些内容,可以覆盖来自 generator 的模板。你可以从 generator repository 复制你需要的模板然后修改,并使用以下代码在插件中配置文件:

openApiGenerate {
      templateDir.set(apiDirectoryPath + File.separator + "templates")
}

我建议添加下面的代码,以确保每次我们使用clean命令时,自动清理我们生成的代码,并且每个build生成的代码都会得到进一步的深入优化。

tasks {
    register("cleanGeneratedCodeTask") {
        description = "Removes generated Open API code"

        doLast {
            File(generatedCodeDirectoryPath).deleteRecursively()
        }
    }

    clean { dependsOn("cleanGeneratedCodeTask"); finalizedBy(openApiGenerate) }
    compileJava { dependsOn(openApiGenerate) }
}

最后,让我们确保所有的代码都包含在我们的 SourceSet 中:

sourceSets[SourceSet.MAIN_SOURCE_SET_NAME].java {
    srcDir(generatedCodeDirectoryPath + File.separator +
         "src" + File.separator + "main" + File.separator + "kotlin")
}

如果build现在在 Gradle 中运行这个命令,你应该会在项目文件中看到类似的内容:

我们能看到代码当前存在错误,因为它找不到javax.validation包。我们可以通过添加build.gradle.kt下面的依赖项轻松解决这个问题:

dependencies {
       implementation("org.springframework.boot:spring-boot-starter-validation")
}

第 3 步 - 实施 API 🎉

这个阶段是目前来说最简单的一步。GreetingApi将在我们的 controller 中实现我们生成的接口。代码:

@RestController
class GreetingApiController : GreetingApi {

    override fun greetName(
        @RequestParam(value = "name", required = false) name: String?
    ): ResponseEntity<GreetResponse> =
        if (name.isNullOrBlank())
            ResponseEntity.ok(GreetResponse("Hello, world!"))
        else
            ResponseEntity.ok(GreetResponse("Hello, $name!"))
}

我们现在可以运行服务器,并尝试在浏览器中调用它:

第 4 步 - 编写测试

现在需要对 API 进行测试,由于我们这里没有实际的业务逻辑,所以只会对我们的新代码进行集成测试。

让我们首先在目录中定义我们的WebConfig类,src/main/kotlin内容如下:

@Configuration
@ComponentScan(
    basePackageClasses = [GreetingApplication::class],
    includeFilters = [ComponentScan.Filter(RestController::class)]
)
class WebConfig

添加我们的测试类。使用@ParameterizedTest jUnit5 的功能来避免代码重复。代码:

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@ExtendWith(SpringExtension::class)
@ContextConfiguration(classes = [WebConfig::class])
@AutoConfigureMockMvc
@WebAppConfiguration
class GreetingApiControllerTest(context: WebApplicationContext) {
    private val mockMvc = MockMvcBuilders
        .webAppContextSetup(context).build()

    data class TestCase(val name: String?, val expectedResult: String)

    private fun getTestCase(): List<Arguments> =
        arrayOf(
            TestCase(name = "test", expectedResult = "Hello, test!"),
            TestCase(name = null, expectedResult = "Hello, world!"),
            TestCase(name = "jack", expectedResult = "Hello, jack!"),
        ).map { Arguments.of(it) }

    @ParameterizedTest
    @MethodSource("getTestCase")
    fun `should return correct greeting`(testCase: TestCase) {
        // Given uri and a request
        val uri = if (testCase.name.isNullOrBlank()) "/v1/greet"
        else "/v1/greet?name=${testCase.name}"

        val request = MockMvcRequestBuilders.get(uri)
            .accept(MediaType.APPLICATION_JSON)

        // When we call the api
        val response =
            mockMvc
                .perform(request)
                .andExpect(MockMvcResultMatchers.status().isOk)
                .andReturn()
                .response
                .contentAsString

        // Then we expect to have a response with the correct greeting
        val actualGreeting = ObjectMapper()
            .readTree(response)["greet"]
            .asText()
        assertEquals(testCase.expectedResult, actualGreeting)
    }
}

如果你的操作方式没有问题,那么通过运行测试,你应该看到有 2 个测试用例成功了。🎉

完整的项目可以在我的 GitHub 仓库中找到,在那里你会看到完整的代码。

订阅
qrcode

订阅

随时随地获取 Apifox 最新动态