應用程序層:

需要記住的關鍵點是,在這些架構中,各層之間存在一個規定的通信流程,必須遵循這一流程才能確保信息的傳遞具有意義。

這意味著請求首先必須經過第一層,然后是第二層,然后是第三層,依此類推。任何請求都不應該跳過層,因為這會擾亂架構的邏輯以及它給我們帶來的組織和模塊化的好處。

代碼

在跳轉到代碼之前,我們先提一下我們實際要構建的內容。我們將為寵物收容所業務構建一個 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 格式匹配)。

每個測試都會檢查是否返回了預期的 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 個主要內容:

你可以看到,使用 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

這里我們有兩種不同類型的請求:

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/

上一篇:

如何設計和開發Web API:開發人員的基本指南

下一篇:

從零開始:如何為你的項目開發自定義API?
#你可能也喜歡這些API文章!

我們有何不同?

API服務商零注冊

多API并行試用

數據驅動選型,提升決策效率

查看全部API→
??

熱門場景實測,選對API

#AI文本生成大模型API

對比大模型API的內容創意新穎性、情感共鳴力、商業轉化潛力

25個渠道
一鍵對比試用API 限時免費

#AI深度推理大模型API

對比大模型API的邏輯推理準確性、分析深度、可視化建議合理性

10個渠道
一鍵對比試用API 限時免費