所有文章 > API设计 > REST API手册:如何构建、测试、使用和记录REST API
REST API手册:如何构建、测试、使用和记录REST API

REST API手册:如何构建、测试、使用和记录REST API

大家好!在这个教程里,我们将深入研究 REST API

我最近写了这篇文章,详细解释了目前常见的几种API类型之间的主要区别。本教程旨在向您展示如何从零开始构建一个REST API的示例。

我们将介绍 Node 和 Express 的基本设置和架构,使用 Supertest 进行单元测试,了解如何使用 React 前端应用程序的 API,最后我们还将使用Swagger等工具来记录API文档。

请记住,我们不会太深入地介绍每种技术的工作原理。我们的目标是为您提供一个关于REST API如何工作的大致概念,它的各个部分是如何相互交互的,以及一个完整的实现可能包含哪些内容。

现在,让我们开始吧!

什么是 REST?

表述性状态传输 (REST) 是一种广泛使用的架构样式,主要用于构建 Web 服务和 API。

RESTful API 旨在实现简单、可扩展且灵活的系统。它们通常用于 Web 和移动应用程序,以及物联网 (IoT) 和微服务架构中。

主要特点:

  1. 无状态:REST API 是无状态的,这意味着每个请求都包含处理它所需的所有信息。这使得扩展 API 变得更加容易,并通过减少在服务器上存储和管理会话数据的需求来提高性能。
  2. 基于资源:REST API 是基于资源的,这意味着每个资源都由唯一的 URI(统一资源标识符)标识,并且可以使用标准 HTTP 方法(如 GET、POST、PUT 和 DELETE)进行访问。
  3. 统一接口:REST API 具有统一的接口,允许客户端使用一组标准化的方法和响应格式与资源交互。这使开发人员能够更轻松地构建和维护 API,同时也使客户端更容易使用它们。
  4. 可缓存:REST API 是可缓存的,这意味着可以缓存响应以提高性能并减少网络流量。
  5. 分层系统:REST API 设计为分层的,这意味着可以在客户端和服务器之间添加代理和网关等中介,而不会影响整个系统。

优点

  • 易于学习和使用:与其他 API 相比,REST API 相对简单易学。
  • 可扩展性:REST API 的无状态特性使其具有高度可扩展性和效率。
  • 灵活性:REST API 非常灵活,可用于构建各种应用程序和系统。
  • 广泛支持:REST API 受到开发工具和框架的广泛支持,因此可以轻松地将它们集成到现有系统中。

缺点

  • 缺乏标准:REST API 缺乏严格的标准可能会导致不一致和互操作性问题。
  • 功能受限:REST API 旨在处理简单的请求和响应,可能不适合更复杂的使用案例。
  • 安全问题:如果实施不当,REST API 可能容易受到安全攻击,例如跨站点脚本 (XSS) 和跨站点请求伪造 (CSRF)。

应用场景:

  • REST API 非常适合构建 Web 和移动应用程序,以及微服务架构和 IoT 系统。
  • 在需要高度可扩展性和灵活性,以及开发人员必须与现有系统和技术进行集成的情况下,REST API特别有用

总之,REST API 是一种用于构建 Web 服务和 API 的流行且广泛使用的架构样式。它们简单、可扩展且灵活,可用于构建各种应用程序和系统。

尽管存在一些限制和担忧,但在许多不同行业和部门中,REST API仍然是构建API的流行且有效的选择。

如何使用 Node 和 Express 构建 REST API

我们的工具

Node.js 是一个开源且跨平台的后端 JavaScript 的运行环境,允许开发人员在 Web 浏览器之外执行 JavaScript 代码。它由 Ryan Dahl 于 2009 年创建,此后成为构建 Web 应用程序、API 和服务器的流行选择。

Node.js 提供了一种事件驱动的非阻塞 I/O 模型,使其既轻量且高效。这种设计使得 Node.js 能够以高性能处理大量数据。此外,Node.js 拥有一个庞大且活跃的社区,提供了大量的库和模块,帮助开发人员更快、更轻松地构建应用程序。

Express.js 是一种流行的 Node.js Web 应用程序框架,用于构建 Web 应用程序和 API。它提供了一组功能和工具,用于构建 Web 服务器、处理 HTTP 请求和响应、将请求路由到特定处理程序、处理中间件等等。

Express 框架以其简洁、灵活和可扩展的特性而广受欢迎,成为使用 Node.js 开发 Web 应用程序开发者的热门选择。

Express.js 的一些主要功能和优势包括:

  • 简约灵活:Express.js 提供了一种简约而灵活的结构,开发人员能够按照他们想要的方式构建应用程序。
  • 路由:Express.js 可以轻松定义用于处理 HTTP 请求的路由并将其映射到特定函数或处理程序。
  • 中间件:Express.js 允许开发人员定义可用于处理常见任务(如身份验证、日志记录、错误处理等)的中间件函数。
  • 强大的 API:Express.js 提供了一个强大的 API 来处理 HTTP 请求和响应,使开发人员能够构建高性能的 Web 应用程序。

我们的架构

对于这个项目,我们将在代码库中遵循 layers 架构。层架构是将关注点和职责划分为不同的文件夹和文件,并且只允许某些文件夹和文件之间直接通信。

关于项目应该包含多少层、每层的名称以及它们应处理的操作,这些都是需要讨论的问题。那么,让我们看看我认为对于我们的示例来说,有哪些好方法。

我们的应用程序将有五个不同的层,它们将按以下方式排序:

应用程序层:

  • 应用层将具有我们服务器的基本设置和与我们的路由(下一层)的连接。
  • routes 层将包含我们所有路由的定义以及与控制器(下一层)的连接。
  • 控制器层将具有我们想要在每个端点中执行的实际逻辑以及与模型层的连接。
  • 模型层将保存与我们的 mock 数据库交互的逻辑。
  • 最后,持久层是我们的数据库所在的位置。

需要记住的关键点是,在这些架构中,各层之间存在一个规定的通信流程,必须遵循这一流程才能确保信息的传递具有意义。

这意味着请求首先必须经过第一层,然后是第二层,然后是第三层,依此类推。任何请求都不应该跳过层,因为这会扰乱架构的逻辑以及它给我们带来的组织和模块化的好处。

代码

在跳转到代码之前,我们先提一下我们实际要构建的内容。我们将为宠物收容所业务构建一个 API。这个宠物收容所需要注册住在收容所的宠物,为此,我们将执行基本的 CRUD 操作(创建、读取、更新和删除)。

现在,是的,让我们开始这件事。创建一个新目录,跳转到该目录,然后运行 :

npm init -y

安装 Express,通过运行将 nodemon 作为开发依赖项安装(Nodemon 是我们将用来运行和测试服务器的工具)。最后, 在本地运行测试我们的服务器。

npm i expressnpm i -D nodemonnpm i cors

App.js

现在创建一个文件并将以下代码放入其中:

app.js
import express from 'express'
import cors from 'cors'

import petRoutes from './pets/routes/pets.routes.js'

const app = express()
const port = 3000

/* Global middlewares */
app.use(cors())
app.use(express.json())

/* Routes */
app.use('/pets', petRoutes)

/* Server setup */
if (process.env.NODE_ENV !== 'test') {
app.listen(port, () => console.log(`⚡️[server]: Server is running at https://localhost:${port}`))
}

export default app

这将作为我们项目的应用层。

在这里,我们基本上是设置我们的服务器,并声明任何命中方向的请求都应该使用我们在目录中声明的路由 (endpoints)。

/pets./pets/routes/pets.routes.js

接下来,在您的项目中创建以下文件夹结构:

路由

跳到 routes 文件夹,创建一个名为pets.routes.js的文件,并将以下代码放入其中:

import express from "express";
import {
listPets,
getPet,
editPet,
addPet,
deletePet,
} from "../controllers/pets.controllers.js";

const router = express.Router();

router.get("/", listPets);

router.get("/:id", getPet);

router.put("/:id", editPet);

router.post("/", addPet);

router.delete("/:id", deletePet);

export default router;

在这个文件中,我们将初始化一个路由器(处理我们的请求并根据端点 URL 相应地引导它们的东西)并设置每个端点。

对于每个端点,我们声明了相应的 HTTP 方法(如 GET、POST 等)和该端点将触发的函数(如 listPets、getPet 等)。每个函数名称都非常明确,因此我们可以很容易地了解每个端点的作用,而无需查看进一步的代码。

最后,我们还声明了哪个端点将接收请求的 URL 参数,如下所示:这里我们说我们将接收宠物的 URL 参数。

router.get("/:id", getPet);

控制器

现在转到 controllers 文件夹,创建一个pets.controllers.js文件,并将以下代码放入其中:

import { getItem, listItems, editItem, addItem, deleteItem } from '../models/pets.models.js'

export const getPet = (req, res) => {
try {
const resp = getItem(parseInt(req.params.id))
res.status(200).json(resp)

} catch (err) {
res.status(500).send(err)
}
}

export const listPets = (req, res) => {
try {
const resp = listItems()
res.status(200).json(resp)

} catch (err) {
res.status(500).send(err)
}
}

export const editPet = (req, res) => {
try {
const resp = editItem(parseInt(req.params.id), req.body)
res.status(200).json(resp)

} catch (err) {
res.status(500).send(err)
}
}

export const addPet = (req, res) => {
try {
const resp = addItem(req.body)
res.status(200).json(resp)

} catch (err) {
res.status(500).send(err)
}
}

export const deletePet = (req, res) => {
try {
const resp = deleteItem(parseInt(req.params.id))
res.status(200).json(resp)

} catch (err) {
res.status(500).send(err)
}
}

控制器是每个端点请求将触发的函数。如您所见,它们接收 request 和 response 对象作为参数。在 request 对象中,我们可以读取 URL 或 body 参数等内容,在完成相应的计算后,我们将使用 response 对象发送我们的响应。

每个控制器都是调用我们模型中定义的特定函数。

模型

现在转到 models 文件夹并创建一个包含此代码的文件:

import db from '../../db/db.js'

export const getItem = id => {
try {
const pet = db?.pets?.filter(pet => pet?.id === id)[0]
return pet
} catch (err) {
console.log('Error', err)
}
}

export const listItems = () => {
try {
return db?.pets
} catch (err) {
console.log('Error', err)
}
}

export const editItem = (id, data) => {
try {
const index = db.pets.findIndex(pet => pet.id === id)

if (index === -1) throw new Error('Pet not found')
else {
db.pets[index] = data
return db.pets[index]
}
} catch (err) {
console.log('Error', err)
}
}

export const addItem = data => {
try {
const newPet = { id: db.pets.length + 1, ...data }
db.pets.push(newPet)
return newPet

} catch (err) {
console.log('Error', err)
}
}

export const deleteItem = id => {
try {
// delete item from db
const index = db.pets.findIndex(pet => pet.id === id)

if (index === -1) throw new Error('Pet not found')
else {
db.pets.splice(index, 1)
return db.pets
}
} catch (error) {

}
}

这些是负责与我们的数据层(数据库)交互并将相应信息返回给我们的控制器的函数。

数据库

在这个示例中,我们不会采用实际的数据库。相反,我们将仅使用一个简单的数组来达到我们的目的。尽管这种方法在每次服务器重新启动时都会重置数据,但它足以满足我们当前的示例需求。

在项目的根目录中,创建一个文件夹和一个文件,其中包含以下代码:

const db = {
pets: [
{
id: 1,
name: 'Rex',
type: 'dog',
age: 3,
breed: 'labrador',
},
{
id: 2,
name: 'Fido',
type: 'dog',
age: 1,
breed: 'poodle',
},
{
id: 3,
name: 'Mittens',
type: 'cat',
age: 2,
breed: 'tabby',
},
]
}

export default db

如你所见,我们的对象包含一个属性,其值是一个对象数组,每个对象都是一个宠物。对于每只宠物,我们都会存储一个 ID、名称、类型、年龄和品种。

(现在请转到您的终端并运行以下命令:

nodemon app.js

您应该会看到以下消息,确认您的服务器正在运行:

[server]: Server is running at https://localhost:3000

如何使用 Supertest 测试 REST API

现在我们的服务器已经启动并运行,让我们实现一个简单的测试套件来检查我们的每个端点是否按预期运行。

我们的工具

SuperTest 是一个 JavaScript 库,用于测试发出 HTTP 请求的 HTTP 服务器或 Web 应用程序。它为测试 HTTP 提供了高级抽象,允许开发人员发送 HTTP 请求并对收到的响应进行断言,从而更轻松地为 Web 应用程序编写自动化测试。

SuperTest 可与任何 JavaScript 测试框架(如 Mocha 或 Jest)配合使用,也可与任何 HTTP 服务器或 Web 应用程序框架(如 Express)一起使用。

SuperTest 建立在流行的测试库 Mocha 之上,并使用 Chai 断言库对收到的响应进行断言。它提供了一个易于使用的 API 来发出 HTTP 请求,包括对身份验证、标头和请求正文的支持。

SuperTest 还允许开发人员测试整个请求/响应周期,包括中间件和错误处理,使其成为测试 Web 应用程序的强大工具。

总体而言,对于希望为其 Web 应用程序编写自动化测试的开发人员来说,SuperTest 是一个有价值的工具。它有助于确保他们的应用程序正常运行,并且他们对代码库所做的任何更改都不会引入新的错误或问题。

代码

首先,我们需要安装一些依赖项。要保存终端命令,请转到您的文件并将您的部分替换为此部分。然后运行:

package.jsondevDependenciesnpm install
  "devDependencies": {
"@babel/core": "^7.21.4",
"@babel/preset-env": "^7.21.4",
"babel-jest": "^29.5.0",
"jest": "^29.5.0",
"jest-babel": "^1.0.1",
"nodemon": "^2.0.22",
"supertest": "^6.3.3"
}

我们将安装 supertestjestbabel 库,这些库对我们的测试运行是必需的,此外还需要一些工具来正确识别项目中的测试文件。

在您的package.json中,添加以下脚本:

  "scripts": {
"test": "jest"
},

为了结束样板代码,请在项目根目录中创建babel.config.cjs文件并将以下代码放入其中:

//babel.config.cjs
module.exports = {
presets: [
[
'@babel/preset-env',
{
targets: {
node: 'current',
},
},
],
],
};

现在让我们编写一些实际测试!在 routes 文件夹中,创建一个包含以下代码的文件:

import supertest from 'supertest' // Import supertest
import server from '../../app' // Import the server object
const requestWithSupertest = supertest(server) // We will use this function to mock HTTP requests

describe('GET "/"', () => {
test('GET "/" returns all pets', async () => {
const res = await requestWithSupertest.get('/pets')
expect(res.status).toEqual(200)
expect(res.type).toEqual(expect.stringContaining('json'))
expect(res.body).toEqual([
{
id: 1,
name: 'Rex',
type: 'dog',
age: 3,
breed: 'labrador',
},
{
id: 2,
name: 'Fido',
type: 'dog',
age: 1,
breed: 'poodle',
},
{
id: 3,
name: 'Mittens',
type: 'cat',
age: 2,
breed: 'tabby',
},
])
})
})

describe('GET "/:id"', () => {
test('GET "/:id" returns given pet', async () => {
const res = await requestWithSupertest.get('/pets/1')
expect(res.status).toEqual(200)
expect(res.type).toEqual(expect.stringContaining('json'))
expect(res.body).toEqual(
{
id: 1,
name: 'Rex',
type: 'dog',
age: 3,
breed: 'labrador',
}
)
})
})

describe('PUT "/:id"', () => {
test('PUT "/:id" updates pet and returns it', async () => {
const res = await requestWithSupertest.put('/pets/1').send({
id: 1,
name: 'Rexo',
type: 'dogo',
age: 4,
breed: 'doberman'
})
expect(res.status).toEqual(200)
expect(res.type).toEqual(expect.stringContaining('json'))
expect(res.body).toEqual({
id: 1,
name: 'Rexo',
type: 'dogo',
age: 4,
breed: 'doberman'
})
})
})

describe('POST "/"', () => {
test('POST "/" adds new pet and returns the added item', async () => {
const res = await requestWithSupertest.post('/pets').send({
name: 'Salame',
type: 'cat',
age: 6,
breed: 'pinky'
})
expect(res.status).toEqual(200)
expect(res.type).toEqual(expect.stringContaining('json'))
expect(res.body).toEqual({
id: 4,
name: 'Salame',
type: 'cat',
age: 6,
breed: 'pinky'
})
})
})

describe('DELETE "/:id"', () => {
test('DELETE "/:id" deletes given pet and returns updated list', async () => {
const res = await requestWithSupertest.delete('/pets/2')
expect(res.status).toEqual(200)
expect(res.type).toEqual(expect.stringContaining('json'))
expect(res.body).toEqual([
{
id: 1,
name: 'Rexo',
type: 'dogo',
age: 4,
breed: 'doberman'
},
{
id: 3,
name: 'Mittens',
type: 'cat',
age: 2,
breed: 'tabby',
},
{
id: 4,
name: 'Salame',
type: 'cat',
age: 6,
breed: 'pinky'
}
])
})
})

对于每个终端节点,测试会发送 HTTP 请求并检查响应是否符合以下三个条件:HTTP 状态代码、响应类型(应为 JSON)和响应正文(应与预期的 JSON 格式匹配)。

  • 第一个测试向 /pets 端点发送 GET 请求,并期望 API 以 JSON 格式返回一个宠物数组。
  • 第二个测试向 /pets/:id 端点发送 GET 请求,并期望 API 以 JSON 格式返回指定 ID 的宠物。
  • 第三个测试向 /pets/:id 端点发送 PUT 请求,并期望 API 更新指定 ID 的宠物,并以 JSON 格式返回更新后的宠物。
  • 第四个测试向 /pets 端点发送 POST 请求,并期望 API 添加一只新的宠物,并以 JSON 格式返回新增的宠物。
  • 最后,第五个测试向 /pets/:id 端点发送 DELETE 请求,并期望 API 删除指定 ID 的宠物,并以 JSON 格式返回更新后的宠物列表。

每个测试都会检查是否返回了预期的 HTTP 状态码、响应类型和响应正文。如果未满足这些预期中的任何一个,则测试将失败并提供错误消息。

这些测试对于确保 API 在不同的 HTTP 请求和终端节点之间正确一致地工作非常重要。测试可以自动运行,从而可以轻松检测 API 功能中的任何问题或回归。

现在转到您的终端运行npm test ,您应该会看到所有测试都通过了:

> restapi@1.0.0 test
> jest

PASS pets/routes/pets.test.js
GET "/"
✓ GET "/" returns all pets (25 ms)
GET "/:id"
✓ GET "/:id" returns given pet (4 ms)
PUT "/:id"
✓ PUT "/:id" updates pet and returns it (15 ms)
POST "/"
✓ POST "/" adds new pet and returns the added item (3 ms)
DELETE "/:id"
✓ DELETE "/:id" deletes given pet and returns updated list (3 ms)

Test Suites: 1 passed, 1 total
Tests: 5 passed, 5 total
Snapshots: 0 total
Time: 1.611 s
Ran all test suites.

如何在前端 React 应用程序上使用 REST API

现在我们知道我们的服务器正在运行,并且我们的端点正在按预期运行。让我们看一些更实际的示例,以了解前端应用程序如何使用我们的 API。

在这个例子中,我们将使用一个 React 应用程序和两个不同的工具来发送和处理我们的请求:Fetch API 和 Axios 库。

我们的工具

React 是一个流行的 JavaScript 库,用于构建用户界面。它允许开发人员创建可重用的 UI 组件,并有效地更新和呈现它们以响应应用程序状态的变化。

Fetch API 是一种现代浏览器 API,允许开发人员从客户端 JavaScript 代码发出异步 HTTP 请求。它为通过网络获取资源提供了一个简单的接口,并支持各种请求和响应类型。

Axios 是一种流行的 JavaScript HTTP 客户端库。它提供了一个简单直观的 API 来发出 HTTP 请求,并支持广泛的功能,包括请求和响应拦截、请求和响应数据的自动转换以及取消请求的能力。它既可以在浏览器中使用,也可以在服务器上使用,并且经常与 React 应用程序结合使用。

代码

让我们通过运行 yarn create vite 并按照终端提示来创建我们的 React 应用。完成后,运行 yarn add axios(我们将使用它来在应用中设置基本的路由)。

App.jsx

将此代码放入您的App.jsx文件中:

import { Suspense, lazy, useState } from 'react'
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom'
import './App.css'

const PetList = lazy(() => import ('./pages/PetList'))
const PetDetail = lazy(() => import ('./pages/PetDetail'))
const EditPet = lazy(() => import ('./pages/EditPet'))
const AddPet = lazy(() => import ('./pages/AddPet'))

function App() {

const [petToEdit, setPetToEdit] = useState(null)

return (
<div className="App">
<Router>
<h1>Pet shelter</h1>

<Link to='/add'>
<button>Add new pet</button>
</Link>

<Routes>
<Route path='/' element={<Suspense fallback={<></>}><PetList /></Suspense>}/>

<Route path='/:petId' element={<Suspense fallback={<></>}><PetDetail setPetToEdit={setPetToEdit} /></Suspense>}/>

<Route path='/:petId/edit' element={<Suspense fallback={<></>}><EditPet petToEdit={petToEdit} /></Suspense>}/>

<Route path='/add' element={<Suspense fallback={<></>}><AddPet /></Suspense>}/>
</Routes>

</Router>
</div>
)
}

export default App

在这里,我们只定义我们的路由。我们的应用程序中将有 4 个主要路由,每个路由对应不同的视图:

  • 一个用于查看整个宠物列表。
  • 一个用于查看单个宠物的详细信息。
  • 一个用于编辑单个宠物。
  • 一个用于将新宠物添加到列表中。

此外,我们还有一个用于添加新宠物的按钮和一个 state,该 state 将存储我们要编辑的宠物的信息。

接下来,创建一个包含这些文件的目录:

图像

PetList.jsx

让我们从负责渲染整个 pets 列表的文件开始:

import { useEffect, useState } from 'react'
import { Link } from 'react-router-dom'
import axios from 'axios'

function PetList() {
const [pets, setPets] = useState([])

const getPets = async () => {
try {
/* FETCH */
// const response = await fetch('http://localhost:3000/pets')
// const data = await response.json()
// if (response.status === 200) setPets(data)

/* AXIOS */
const response = await axios.get('http://localhost:3000/pets')
if (response.status === 200) setPets(response.data)

} catch (error) {
console.error('error', error)
}
}

useEffect(() => { getPets() }, [])

return (
<>
<h2>Pet List</h2>

{pets?.map((pet) => {
return (
<div key={pet?.id}>
<p>{pet?.name} - {pet?.type} - {pet?.breed}</p>

<Link to={`/${pet?.id}`}>
<button>Pet detail</button>
</Link>
</div>
)
})}
</>
)
}

export default PetList

正如你所看到的,从逻辑上讲,我们这里有 3 个主要内容:

  • 存储要渲染的宠物列表的状态。
  • 一个函数,用于执行对 API 的相应请求。
  • 一个 useEffect,在组件渲染时执行该函数。

你可以看到,使用 fetch 和 Axios 发出 HTTP 请求的语法非常相似,但 Axios 更简洁一些。发出请求后,我们检查状态是否为 200(表示成功),并将响应存储在我们的状态中。

一旦我们的状态被更新,组件将呈现我们的 API 提供的数据。

要调用我们的服务器,必须确保服务器已经启动。请在服务器项目的终端中运行 nodemon app.js 以启动服务器。

PetDetail.jsx

现在让我们转到PetDetail.jsx文件:

import { useEffect, useState } from 'react'
import { useParams, Link } from 'react-router-dom'
import axios from 'axios'

function PetDetail({ setPetToEdit }) {

const [pet, setPet] = useState([])

const { petId } = useParams()

const getPet = async () => {
try {
/* FETCH */
// const response = await fetch(`http://localhost:3000/pets/${petId}`)
// const data = await response.json()
// if (response.status === 200) {
// setPet(data)
// setPetToEdit(data)
// }

/* AXIOS */
const response = await axios.get(`http://localhost:3000/pets/${petId}`)
if (response.status === 200) {
setPet(response.data)
setPetToEdit(response.data)
}

} catch (error) {
console.error('error', error)
}
}

useEffect(() => { getPet() }, [])

const deletePet = async () => {
try {
/* FETCH */
// const response = await fetch(`http://localhost:3000/pets/${petId}`, {
// method: 'DELETE'
// })

/* AXIOS */
const response = await axios.delete(`http://localhost:3000/pets/${petId}`)

if (response.status === 200) window.location.href = '/'
} catch (error) {
console.error('error', error)
}
}

return (
<div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', aligniItems: 'center' }}>
<h2>Pet Detail</h2>

{pet && (
<>
<p>Pet name: {pet.name}</p>
<p>Pet type: {pet.type}</p>
<p>Pet age: {pet.age}</p>
<p>Pet breed: {pet.breed}</p>

<div style={{ display: 'flex', justifyContent: 'center', aligniItems: 'center' }}>

<Link to={`/${pet?.id}/edit`}>
<button style={{ marginRight: 10 }}>Edit pet</button>
</Link>

<button
style={{ marginLeft: 10 }}
onClick={() => deletePet()}
>
Delete pet
</button>
</div>
</>
)}
</div>
)
}

export default PetDetail

这里我们有两种不同类型的请求:

  • 一种是获取给定宠物的信息(其行为与之前我们看到的请求非常相似)。唯一的区别是我们这里向端点传递了一个URL参数,同时我们在前端应用中从URL读取该参数。
  • 另一个请求是从我们的登记册中删除给定的宠物。这里的区别在于,一旦我们确认请求成功,我们就会将用户重定向到我们应用程序的根目录。

AddPet.jsx

这是负责将新宠物添加到我们的注册中的文件:

import React, { useState } from 'react'
import axios from 'axios'

function AddPet() {

const [petName, setPetName] = useState()
const [petType, setPetType] = useState()
const [petAge, setPetAge] = useState()
const [petBreed, setPetBreed] = useState()

const addPet = async () => {
try {
const petData = {
name: petName,
type: petType,
age: petAge,
breed: petBreed
}

/* FETCH */
// const response = await fetch('http://localhost:3000/pets/', {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json'
// },
// body: JSON.stringify(petData)
// })

// if (response.status === 200) {
// const data = await response.json()
// window.location.href = `/${data.id}`
// }

/* AXIOS */
const response = await axios.post(
'http://localhost:3000/pets/',
petData,
{ headers: { 'Content-Type': 'application/json' } }
)

if (response.status === 200) window.location.href = `/${response.data.id}`

} catch (error) {
console.error('error', error)
}
}

return (
<div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', aligniItems: 'center' }}>
<h2>Add Pet</h2>

<div style={{ display: 'flex', flexDirection: 'column', margin: 20 }}>
<label>Pet name</label>
<input type='text' value={petName} onChange={e => setPetName(e.target.value)} />
</div>

<div style={{ display: 'flex', flexDirection: 'column', margin: 20 }}>
<label>Pet type</label>
<input type='text' value={petType} onChange={e => setPetType(e.target.value)} />
</div>

<div style={{ display: 'flex', flexDirection: 'column', margin: 20 }}>
<label>Pet age</label>
<input type='text' value={petAge} onChange={e => setPetAge(e.target.value)} />
</div>

<div style={{ display: 'flex', flexDirection: 'column', margin: 20 }}>
<label>Pet breed</label>
<input type='text' value={petBreed} onChange={e => setPetBreed(e.target.value)} />
</div>

<button
style={{ marginTop: 30 }}
onClick={() => addPet()}
>
Add pet
</button>
</div>
)
}

export default AddPet

在这里,我们呈现了一个表单,用户需要在其中输入新的宠物信息。

我们为每个要输入的信息设置了一个状态,在我们的请求中,我们使用这些状态构建一个对象。这个对象将作为我们请求的正文。

在我们的请求中,我们检查响应是否成功。如果成功,我们将重定向到新添加宠物的详细页面。为了重定向,我们使用了HTTP响应中返回的ID。

编辑Pet.jsx

最后,负责编辑宠物登记册的文件:

import React, { useState } from 'react'
import axios from 'axios'

function EditPet({ petToEdit }) {

const [petName, setPetName] = useState(petToEdit?.name)
const [petType, setPetType] = useState(petToEdit?.type)
const [petAge, setPetAge] = useState(petToEdit?.age)
const [petBreed, setPetBreed] = useState(petToEdit?.breed)

const editPet = async () => {
try {
const petData = {
id: petToEdit.id,
name: petName,
type: petType,
age: petAge,
breed: petBreed
}

/* FETCH */
// const response = await fetch(`http://localhost:3000/pets/${petToEdit.id}`, {
// method: 'PUT',
// headers: {
// 'Content-Type': 'application/json'
// },
// body: JSON.stringify(petData)
// })

/* AXIOS */
const response = await axios.put(
`http://localhost:3000/pets/${petToEdit.id}`,
petData,
{ headers: { 'Content-Type': 'application/json' } }
)

if (response.status === 200) {
window.location.href = `/${petToEdit.id}`
}
} catch (error) {
console.error('error', error)
}
}

return (
<div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', aligniItems: 'center' }}>
<h2>Edit Pet</h2>

<div style={{ display: 'flex', flexDirection: 'column', margin: 20 }}>
<label>Pet name</label>
<input type='text' value={petName} onChange={e => setPetName(e.target.value)} />
</div>

<div style={{ display: 'flex', flexDirection: 'column', margin: 20 }}>
<label>Pet type</label>
<input type='text' value={petType} onChange={e => setPetType(e.target.value)} />
</div>

<div style={{ display: 'flex', flexDirection: 'column', margin: 20 }}>
<label>Pet age</label>
<input type='text' value={petAge} onChange={e => setPetAge(e.target.value)} />
</div>

<div style={{ display: 'flex', flexDirection: 'column', margin: 20 }}>
<label>Pet breed</label>
<input type='text' value={petBreed} onChange={e => setPetBreed(e.target.value)} />
</div>

<button
style={{ marginTop: 30 }}
onClick={() => editPet()}
>
Save changes
</button>
</div>
)
}

export default EditPet

这和处理文件的方式非常相似。唯一的区别是我们的 pet info 状态是用我们要编辑的 pet 的值初始化的。当用户更新这些值时,我们构造一个对象,该对象将成为我们的请求正文,并发送包含更新信息的请求。很简单。

就是这样!我们在前端应用程序中使用了所有的 API 端点。

如何使用 Swagger 记录 REST API

现在我们已经启动并运行、测试了服务器,并将其连接到了前端应用程序,接下来我们要完成的最后一步是记录我们的 API。

文档和 API 通常包括声明哪些终端节点可用,每个终端节点执行哪些操作,以及每个终端节点的参数和返回值。

这不仅能帮助我们记住服务器的工作原理,而且对于那些想要与我们 API 交互的人来说也非常有用。

例如,在一个公司中,通常会有后端团队和前端团队。当 API 正在开发并需要与前端应用程序集成时,询问哪个端点做什么以及应该传递哪些参数将非常乏味。如果你把这些信息都放在一个地方,就可以直接去阅读。这就是文档的作用。

我们的工具

Swagger 是一组开源工具,可帮助开发人员构建、记录和使用 RESTful Web 服务。它为用户提供了一个用户友好的图形界面来与 API 进行交互,还为各种编程语言生成客户端代码,使 API 集成更容易。

Swagger 为 API 开发提供了一套全面的功能,包括 API 设计、文档、测试和代码生成。它允许开发人员使用 OpenAPI 规范以标准化方式定义 API 端点、输入参数、预期输出和身份验证要求。

Swagger UI 是一种流行的工具,它将 OpenAPI 规范呈现为交互式 API 文档,允许开发人员通过 Web 浏览器探索和测试 API。它提供了一个用户友好的界面,使开发人员能够轻松查看 API 端点并与之交互。

如何实现 Swagger

在我们的服务器应用中,要实现Swagger,我们需要添加两个新的依赖项。所以运行以下命令:

npm i swagger-jsdoc
npm i swagger-ui-express

接下来,将app.js文件修改为如下所示:

import express from 'express'
import cors from 'cors'
import swaggerUI from 'swagger-ui-express'
import swaggerJSdoc from 'swagger-jsdoc'

import petRoutes from './pets/routes/pets.routes.js'

const app = express()
const port = 3000

// swagger definition
const swaggerSpec = {
definition: {
openapi: '3.0.0',
info: {
title: 'Pets API',
version: '1.0.0',
},
servers: [
{
url: `http://localhost:${port}`,
}
]
},
apis: ['./pets/routes/*.js'],
}

/* Global middlewares */
app.use(cors())
app.use(express.json())
app.use(
'/api-docs',
swaggerUI.serve,
swaggerUI.setup(swaggerJSdoc(swaggerSpec))
)

/* Routes */
app.use('/pets', petRoutes)

/* Server setup */
if (process.env.NODE_ENV !== 'test') {
app.listen(port, () => console.log(`⚡️[server]: Server is running at https://localhost:${port}`))
}

export default app

如你所见,我们正在导入新的依赖项,我们正在创建一个包含我们实现的配置选项的对象,然后设置一个中间件来在我们的应用程序目录中呈现我们的文档。

到目前为止,如果您打开浏览器并转到 http://localhost:3000/api-docs/ 您应该会看到以下内容:

Swagger 很酷的地方在于它为我们的文档提供了一个开箱即用的 UI,您可以在配置中声明的 URL 路径中轻松访问它。

现在让我们编写一些实际的文档!

跳转到pets.routes.js文件并将其代码替换为以下内容:

import express from "express";
import {
listPets,
getPet,
editPet,
addPet,
deletePet,
} from "../controllers/pets.controllers.js";

const router = express.Router();

/**
* @swagger
* components:
* schemas:
* Pet:
* type: object
* properties:
* id:
* type: integer
* description: Pet id
* name:
* type: string
* description: Pet name
* age:
* type: integer
* description: Pet age
* type:
* type: string
* description: Pet type
* breed:
* type: string
* description: Pet breed
* example:
* id: 1
* name: Rexaurus
* age: 3
* breed: labrador
* type: dog
*/

/**
* @swagger
* /pets:
* get:
* summary: Get all pets
* description: Get all pets
* responses:
* 200:
* description: Success
* 500:
* description: Internal Server Error
*/
router.get("/", listPets);

/**
* @swagger
* /pets/{id}:
* get:
* summary: Get pet detail
* description: Get pet detail
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: Pet id
* responses:
* 200:
* description: Success
* 500:
* description: Internal Server Error
*/
router.get("/:id", getPet);

/**
* @swagger
* /pets/{id}:
* put:
* summary: Edit pet
* description: Edit pet
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: Pet id
* requestBody:
* description: A JSON object containing pet information
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Pet'
* example:
* name: Rexaurus
* age: 12
* breed: labrador
* type: dog
* responses:
* 200:
* description: Success
* 500:
* description: Internal Server Error
*
*/
router.put("/:id", editPet);

/**
* @swagger
* /pets:
* post:
* summary: Add pet
* description: Add pet
* requestBody:
* description: A JSON object containing pet information
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Pet'
* example:
* name: Rexaurus
* age: 12
* breed: labrador
* type: dog
* responses:
* 200:
* description: Success
* 500:
* description: Internal Server Error
*/
router.post("/", addPet);

/**
* @swagger
* /pets/{id}:
* delete:
* summary: Delete pet
* description: Delete pet
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: Pet id
* responses:
* 200:
* description: Success
* 500:
* description: Internal Server Error
*/
router.delete("/:id", deletePet);

export default router;

如您所见,我们为每个终端节点添加了一种特殊类型的注释。这是 Swagger UI 在我们的代码中识别文档的方式。我们将它们放在此文件中,因为让文档尽可能靠近端点是有意义的,但您可以将它们放在您想要的任何位置。

如果我们详细分析这些注释,你可以看到它们是用类似 YAML 的语法编写的,并且对于每个注释,我们指定了端点路由、HTTP 方法、描述、它接收的参数和可能的响应。

除了第一个评论外,所有评论都或多或少相同。在这个 API 中,我们定义了一个 “schema”,它就像对一种对象进行键入,我们稍后可以在其他注释中重用。在我们的例子中,我们定义了 “Pet” 架构,然后将其用于 and 端点。

如果您再次输入 http://localhost:3000/api-docs/,您现在应该会看到以下内容:

每个端点都可以扩展,如下所示:

如果我们单击 “Try it out” 按钮,我们可以执行 HTTP 请求并查看响应是什么样子的:

这对于一般开发人员和想要使用我们的 API 的人来说非常有用,而且如您所见,设置非常简单。

拥有即用型用户界面(UI)确实简化了与文档的互动。在我们的代码库中,这也带来了巨大的优势,因为我们可以自由地修改和更新UI,而无需涉及其他任何外部代码。

结束语

希望您喜欢这本手册,并能从中学到一些新知识。

原文链接:https://www.freecodecamp.org/news/build-consume-and-document-a-rest-api/

#你可能也喜欢这些API文章!