發(fā)布于:2020-12-24 16:05:14
0
76
0
REST API是當(dāng)今最常見的Web服務(wù)之一。它們?cè)试S包括瀏覽器應(yīng)用程序在內(nèi)的各種客戶端通過REST API與服務(wù)器進(jìn)行通信。
因此,正確設(shè)計(jì)REST API非常重要,這樣我們就不會(huì)遇到麻煩。我們必須考慮API使用者的安全性,性能和易用性。
否則,我們會(huì)給使用我們的API的客戶帶來麻煩,這令人不愉快,并且會(huì)分散人們使用我們的API的注意力。如果我們不遵循公認(rèn)的約定,那么我們會(huì)混淆API的維護(hù)者和使用它們的客戶,因?yàn)樗c每個(gè)人的期望都不同。
在本文中,我們將研究如何設(shè)計(jì)REST API,以使使用它們的任何人都易于理解,面向未來,安全,快速,因?yàn)樗鼈兿蚩赡苁菣C(jī)密的客戶提供數(shù)據(jù)。
因?yàn)槁?lián)網(wǎng)應(yīng)用程序有多種破壞方法,所以我們應(yīng)確保使用標(biāo)準(zhǔn)的HTTP代碼,任何REST API都會(huì)優(yōu)雅地處理錯(cuò)誤,以幫助消費(fèi)者解決問題。
接受并使用JSON進(jìn)行響應(yīng)
REST API應(yīng)該接受JSON作為請(qǐng)求有效負(fù)載,并向JSON發(fā)送響應(yīng)。 JSON是用于傳輸數(shù)據(jù)的標(biāo)準(zhǔn)。幾乎每種聯(lián)網(wǎng)技術(shù)都可以使用它:JavaScript具有內(nèi)置方法,可以通過Fetch API或其他HTTP客戶端對(duì)JSON進(jìn)行編碼和解碼。服務(wù)器端技術(shù)具有無需大量工作即可解碼JSON的庫(kù)。
還有其他傳輸數(shù)據(jù)的方法。如果不將數(shù)據(jù)本身轉(zhuǎn)換為可以使用的東西(通常是JSON),框架就不會(huì)廣泛支持XML。我們無法在客戶端(尤其是在瀏覽器中)如此輕松地操作這些數(shù)據(jù)。為了進(jìn)行正常的數(shù)據(jù)傳輸,最終要付出很多額外的工作。
表單數(shù)據(jù)非常適合發(fā)送數(shù)據(jù),尤其是當(dāng)我們要發(fā)送文件時(shí)。但是對(duì)于文本和數(shù)字,我們不需要表單數(shù)據(jù)來傳輸它們,因?yàn)樵诖蠖鄶?shù)框架中,我們可以通過直接從客戶端獲取JSON來傳輸JSON。到目前為止,這是最簡(jiǎn)單的方法。
為了確保當(dāng)我們的REST API應(yīng)用使用JSON響應(yīng)時(shí),客戶端會(huì)這樣解釋,我們應(yīng)該在發(fā)出請(qǐng)求后將響應(yīng)頭中的Content-Type設(shè)置為application / json。許多服務(wù)器端應(yīng)用程序框架會(huì)自動(dòng)設(shè)置響應(yīng)頭。一些HTTP客戶端查看Content-Type響應(yīng)標(biāo)頭,并根據(jù)該格式解析數(shù)據(jù)。
唯一的例外是,如果我們嘗試在客戶端和服務(wù)器之間發(fā)送和接收文件。然后,我們需要處理文件響應(yīng)并將表單數(shù)據(jù)從客戶端發(fā)送到服務(wù)器。但這是另一個(gè)話題。
我們還應(yīng)確保端點(diǎn)返回JSON作為響應(yīng)。許多服務(wù)器端框架都將此作為內(nèi)置功能。
讓我們看一下一個(gè)接受JSON有效負(fù)載的API示例。此示例將對(duì)Node.js使用Express后端框架。我們可以使用body-parser中間件來解析JSON請(qǐng)求主體,然后可以使用要返回的對(duì)象作為JSON響應(yīng)調(diào)用res.json方法,如下所示:
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
app.use(bodyParser.json());
app.post('/', (req, res) => {
res.json(req.body);
});
app.listen(3000, () => console.log('server started'));
bodyParser.json()將JSON請(qǐng)求主體字符串解析為JavaScript對(duì)象,然后將其分配給req.body對(duì)象。
在對(duì)application / json的響應(yīng)中設(shè)置Content-Type標(biāo)頭; charset = utf-8,無任何更改。 上面的方法適用于大多數(shù)其他后端框架。
在端點(diǎn)路徑中使用名詞代替動(dòng)詞
我們不應(yīng)該在端點(diǎn)路徑中使用動(dòng)詞。相反,我們應(yīng)該使用表示要檢索或操縱的端點(diǎn)的實(shí)體的名詞作為路徑名。
這是因?yàn)槲覀兊腍TTP請(qǐng)求方法已經(jīng)有動(dòng)詞。在API端點(diǎn)路徑中使用動(dòng)詞是沒有用的,而且由于它不會(huì)傳達(dá)任何新信息,因此它會(huì)不必要地變長(zhǎng)。所選動(dòng)詞可能會(huì)因開發(fā)人員的想法而異。例如,有些像“ get”,有些像“ retrieve”,所以最好讓HTTP GET動(dòng)詞告訴我們什么和端點(diǎn)做什么。
該操作應(yīng)由我們正在執(zhí)行的HTTP請(qǐng)求方法指示。最常見的方法包括GET,POST,PUT和DELETE。
GET檢索資源。 POST將新數(shù)據(jù)提交到服務(wù)器。 PUT更新現(xiàn)有數(shù)據(jù)。 DELETE刪除數(shù)據(jù)。這些動(dòng)詞映射到CRUD操作。
牢記上面討論的兩個(gè)原則,我們應(yīng)該創(chuàng)建諸如GET / articles /之類的路由來獲取新聞文章。同樣,POST / articles /用于添加新文章,PUT / articles /:id用于更新具有給定id的文章。 DELETE / articles /:id用于刪除具有給定ID的現(xiàn)有文章。
/ articles代表REST API資源。例如,我們可以使用Express添加以下端點(diǎn)來操縱文章,如下所示:
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
app.use(bodyParser.json());
app.get('/articles', (req, res) => {
const articles = [];
// code to retrieve an article...
res.json(articles);
});
app.post('/articles', (req, res) => {
// code to add a new article...
res.json(req.body);
});
app.put('/articles/:id', (req, res) => {
const { id } = req.params;
// code to update an article...
res.json(req.body);
});
app.delete('/articles/:id', (req, res) => {
const { id } = req.params;
// code to delete an article...
res.json({ deleted: id });
});
app.listen(3000,()=> console.log('服務(wù)器已啟動(dòng)'));
在上面的代碼中,我們定義了端點(diǎn)來操縱文章。 如我們所見,路徑名中沒有任何動(dòng)詞。 我們只有名詞。 這些動(dòng)詞在HTTP動(dòng)詞中。
POST,PUT和DELETE端點(diǎn)都將JSON作為請(qǐng)求正文,并且都返回JSON作為響應(yīng),包括GET端點(diǎn)。
具有多個(gè)名詞的名稱集合
我們應(yīng)該用復(fù)數(shù)名詞來命名集合。 我們不經(jīng)常只想獲得一個(gè)項(xiàng)目,因此我們應(yīng)該與命名保持一致,應(yīng)該使用復(fù)數(shù)名詞。
我們使用復(fù)數(shù)形式來與數(shù)據(jù)庫(kù)中的內(nèi)容保持一致。 表通常具有多個(gè)條目,并對(duì)其進(jìn)行命名以反映這一點(diǎn),因此,為了與表保持一致,我們應(yīng)該使用與API訪問的表相同的語言。
使用/ articles端點(diǎn),所有端點(diǎn)都有復(fù)數(shù)形式,因此我們不必將其更改為復(fù)數(shù)形式。
嵌套分層對(duì)象的資源
處理嵌套資源的端點(diǎn)的路徑應(yīng)通過將嵌套資源附加為父資源后面的路徑名稱來完成。
我們必須確保它確保我們認(rèn)為嵌套資源與數(shù)據(jù)庫(kù)表中的資源匹配。 否則,會(huì)造成混亂。
例如,如果我們希望端點(diǎn)獲取新聞文章的評(píng)論,則應(yīng)將/ comments路徑附加到/ articles路徑的末尾。 這是假設(shè)我們?cè)跀?shù)據(jù)庫(kù)中作為文章的子項(xiàng)擁有評(píng)論。
例如,我們可以使用Express中的以下代碼來做到這一點(diǎn):
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
app.use(bodyParser.json());
app.get('/articles/:articleId/comments', (req, res) => {
const { articleId } = req.params;
const comments = [];
// code to get comments by articleId
res.json(comments);
});
app.listen(3000,()=> console.log('服務(wù)器已啟動(dòng)'));
在上面的代碼中,我們可以在路徑“ / articles /:articleId / comments”上使用GET方法。 我們會(huì)獲得對(duì)由articleId標(biāo)識(shí)的文章的評(píng)論,然后在響應(yīng)中將其返回。 我們?cè)凇?/ articles /:articleId”路徑段之后添加“注釋”,以表明它是/ articles的子資源。
這是有道理的,因?yàn)樵u(píng)論是文章的子對(duì)象,假設(shè)每個(gè)文章都有自己的評(píng)論。 否則,這會(huì)使用戶感到困惑,因?yàn)檫@種結(jié)構(gòu)通常被認(rèn)為是用于訪問子對(duì)象的。 相同的原則也適用于POST,PUT和DELETE端點(diǎn)。 它們都可以為路徑名使用相同的嵌套結(jié)構(gòu)。
妥善處理錯(cuò)誤并返回標(biāo)準(zhǔn)錯(cuò)誤代碼
為避免API用戶在發(fā)生錯(cuò)誤時(shí)產(chǎn)生混淆,我們應(yīng)該適當(dāng)?shù)靥幚礤e(cuò)誤,并返回表明發(fā)生了哪種錯(cuò)誤的HTTP響應(yīng)代碼。 這為API的維護(hù)者提供了足夠的信息來了解發(fā)生的問題。 我們不希望錯(cuò)誤導(dǎo)致系統(tǒng)崩潰,因此我們可以不處理它們,這意味著API使用者必須處理它們。
常見的錯(cuò)誤HTTP狀態(tài)代碼包括:
400錯(cuò)誤的請(qǐng)求–這意味著客戶端輸入驗(yàn)證失敗。
401未經(jīng)授權(quán)-這意味著用戶無權(quán)訪問資源。 通常在用戶未通過身份驗(yàn)證時(shí)返回。
403禁止訪問-表示用戶已通過身份驗(yàn)證,但不允許訪問資源。
404 Not Found –表示找不到資源。
500內(nèi)部服務(wù)器錯(cuò)誤–這是一般服務(wù)器錯(cuò)誤。 它可能不應(yīng)該明確地拋出。
502錯(cuò)誤的網(wǎng)關(guān)-這表明來自上游服務(wù)器的無效響應(yīng)。
503服務(wù)不可用–這表示服務(wù)器端發(fā)生了意外情況(可能是服務(wù)器過載,系統(tǒng)某些部分發(fā)生故障等)。
我們應(yīng)該拋出與我們的應(yīng)用程序遇到的問題相對(duì)應(yīng)的錯(cuò)誤。 例如,如果我們想拒絕請(qǐng)求有效載荷中的數(shù)據(jù),那么我們應(yīng)該在Express API中返回如下所示的400響應(yīng):
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
// existing users
const users = [
{ email: 'abc@foo.com' }
]
app.use(bodyParser.json());
app.post('/users', (req, res) => {
const { email } = req.body;
const userExists = users.find(u => u.email === email);
if (userExists) {
return res.status(400).json({ error: 'User already exists' })
}
res.json(req.body);
});
app.listen(3000,()=> console.log('服務(wù)器已啟動(dòng)'));
在上面的代碼中,我們?cè)谟脩魯?shù)組中包含給定電子郵件的現(xiàn)有用戶列表。
然后,如果我們嘗試使用用戶已經(jīng)存在的電子郵件值提交有效負(fù)載,則會(huì)收到帶有“用戶已存在”消息的400響應(yīng)狀態(tài)代碼,以告知用戶該用戶已經(jīng)存在。 利用這些信息,用戶可以通過將電子郵件更改為不存在的郵件來糾正操作。
錯(cuò)誤代碼需要附帶消息,以便維護(hù)人員有足夠的信息來解決問題,但是攻擊者無法使用錯(cuò)誤內(nèi)容來進(jìn)行攻擊,例如竊取信息或關(guān)閉系統(tǒng)。
每當(dāng)我們的API未成功完成時(shí),我們都應(yīng)通過發(fā)送錯(cuò)誤信息并幫助用戶采取糾正措施來正常地失敗。
允許過濾,排序和分頁
REST API背后的數(shù)據(jù)庫(kù)可能會(huì)非常龐大。 有時(shí),有太多數(shù)據(jù),因此不應(yīng)立即全部返回,因?yàn)樗驎?huì)導(dǎo)致系統(tǒng)崩潰。 因此,我們需要過濾項(xiàng)目的方法。
我們還需要分頁數(shù)據(jù)的方式,以便一次只返回一些結(jié)果。 我們不想通過嘗試一次獲取所有請(qǐng)求的數(shù)據(jù)來占用資源太長(zhǎng)時(shí)間。
過濾和分頁都通過減少服務(wù)器資源的使用來提高性能。 隨著數(shù)據(jù)庫(kù)中積累的數(shù)據(jù)越多,這些功能就越重要。
這是一個(gè)小示例,其中API可以接受帶有各種查詢參數(shù)的查詢字符串,以使我們可以根據(jù)項(xiàng)目的字段來過濾出項(xiàng)目:
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
// employees data in a database
const employees = [
{ firstName: 'Jane', lastName: 'Smith', age: 20 },
//...
{ firstName: 'John', lastName: 'Smith', age: 30 },
{ firstName: 'Mary', lastName: 'Green', age: 50 },
]
app.use(bodyParser.json());
app.get('/employees', (req, res) => {
const { firstName, lastName, age } = req.query;
let results = [...employees];
if (firstName) {
results = results.filter(r => r.firstName === firstName);
}
if (lastName) {
results = results.filter(r => r.lastName === lastName);
}
if (age) {
results = results.filter(r => +r.age === +age);
}
res.json(results);
});
app.listen(3000,()=> console.log('服務(wù)器已啟動(dòng)'));
在上面的代碼中,我們有req.query變量來獲取查詢參數(shù)。 然后,我們使用JavaScript解構(gòu)語法通過將各個(gè)查詢參數(shù)解構(gòu)為變量來提取屬性值。 最后,我們對(duì)每個(gè)查詢參數(shù)值運(yùn)行filter以找到我們要返回的項(xiàng)目。
完成此操作后,我們將結(jié)果作為響應(yīng)返回。 因此,當(dāng)我們使用查詢字符串向以下路徑發(fā)出GET請(qǐng)求時(shí):
/ employees?lastName = Smith&age = 30
我們得到:
[
{
"firstName": "John",
"lastName": "Smith",
"age": 30
}
]
作為返回的響應(yīng),因?yàn)槲覀儼磍astName和age進(jìn)行了過濾。
同樣,我們可以接受頁面查詢參數(shù),并在(page-1)* 20到page * 20的位置返回一組條目。
我們還可以在查詢字符串中指定要排序的字段。 例如,我們可以從查詢字符串中獲取參數(shù),其中包含我們要為其排序數(shù)據(jù)的字段。 然后,我們可以按照這些單獨(dú)的字段對(duì)它們進(jìn)行排序。例如,我們可能想從URL中提取查詢字符串,例如:http://example.com/articles?sort=+author,發(fā)布日期,其中+表示上升,而-表示下降。 因此,我們按作者姓名的字母順序排序,并且發(fā)布日期從最近到最近。
保持良好的安全習(xí)慣
客戶端和服務(wù)器之間的大多數(shù)通信應(yīng)該是私有的,因?yàn)槲覀兘?jīng)常發(fā)送和接收私有信息。因此,必須使用SSL / TLS進(jìn)行安全保護(hù)。
SSL證書很難加載到服務(wù)器上,而且費(fèi)用是免費(fèi)的或非常低。沒有理由不讓我們的REST API通過安全的渠道而不是公開的方式進(jìn)行通信。
人們不應(yīng)該能夠訪問他們要求的更多信息。例如,普通用戶不應(yīng)訪問其他用戶的信息。他們也不應(yīng)訪問管理員的數(shù)據(jù)。
為了實(shí)施最小特權(quán)原則,我們需要為單個(gè)角色添加角色檢查,或者為每個(gè)用戶添加更精細(xì)的角色。
如果我們選擇將用戶分為幾個(gè)角色,則這些角色應(yīng)具有覆蓋他們所有需求的權(quán)限,而不再需要其他權(quán)限。如果我們對(duì)用戶可以訪問的每個(gè)功能具有更細(xì)化的權(quán)限,那么我們必須確保管理員可以相應(yīng)地向每個(gè)用戶添加和刪除這些功能。另外,我們需要添加一些可應(yīng)用于組用戶的預(yù)設(shè)角色,這樣就不必手動(dòng)為每個(gè)用戶執(zhí)行此操作。
緩存數(shù)據(jù)以提高性能
我們可以添加緩存以從本地內(nèi)存緩存返回?cái)?shù)據(jù),而不是每次我們想要檢索用戶請(qǐng)求的某些數(shù)據(jù)時(shí)都查詢數(shù)據(jù)庫(kù)以獲取數(shù)據(jù)。 緩存的好處是用戶可以更快地獲取數(shù)據(jù)。 但是,用戶獲取的數(shù)據(jù)可能已過時(shí)。 當(dāng)在生產(chǎn)環(huán)境中進(jìn)行調(diào)試時(shí),當(dāng)我們不斷看到舊數(shù)據(jù)時(shí)出現(xiàn)問題時(shí),這也可能導(dǎo)致問題。
緩存解決方案有很多種類,例如Redis,內(nèi)存緩存等等。 隨著需求的變化,我們可以更改數(shù)據(jù)緩存的方式。
例如,Express具有apicache中間件,無需太多配置即可向我們的應(yīng)用程序添加緩存。 我們可以像這樣在服務(wù)器中添加一個(gè)簡(jiǎn)單的內(nèi)存緩存:
const express = require('express');
const bodyParser = require('body-parser');
const apicache = require('apicache');
const app = express();
let cache = apicache.middleware;
app.use(cache('5 minutes'));
// employees data in a database
const employees = [
{ firstName: 'Jane', lastName: 'Smith', age: 20 },
//...
{ firstName: 'John', lastName: 'Smith', age: 30 },
{ firstName: 'Mary', lastName: 'Green', age: 50 },
]
app.use(bodyParser.json());
app.get('/employees', (req, res) => {
res.json(employees);
});
app.listen(3000,()=> console.log('服務(wù)器已啟動(dòng)'));
上面的代碼僅使用apicache.middleware引用了apicache中間件,然后得到:app.use(緩存('5分鐘'))將緩存應(yīng)用于整個(gè)應(yīng)用。 例如,我們將結(jié)果緩存五分鐘。 我們可以根據(jù)需要進(jìn)行調(diào)整。
版本化我們的API
如果我們對(duì)API進(jìn)行任何可能會(huì)破壞客戶端的更改,則應(yīng)該使用不同版本的API。 可以像當(dāng)今大多數(shù)應(yīng)用程序一樣,根據(jù)語義版本(例如,表示主要版本2和第六個(gè)補(bǔ)丁的2.0.6)完成版本控制。
這樣,我們可以逐步淘汰舊的終結(jié)點(diǎn),而不必強(qiáng)迫所有人同時(shí)遷移到新的API。 v1端點(diǎn)可以為那些不想更改的用戶保持活動(dòng)狀態(tài),而v2具有其閃亮的新功能可以為準(zhǔn)備升級(jí)的用戶提供服務(wù)。 如果我們的API是公開的,這一點(diǎn)尤其重要。 我們應(yīng)該對(duì)其進(jìn)行版本控制,以免破壞使用我們API的第三方應(yīng)用。
通常通過在API路徑的開頭添加/ v1 /,/ v2 /等來完成版本控制。
例如,我們可以使用Express進(jìn)行如下操作:
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
app.use(bodyParser.json());
app.get('/v1/employees', (req, res) => {
const employees = [];
// code to get employees
res.json(employees);
});
app.get('/v2/employees', (req, res) => {
const employees = [];
// different code to get employees
res.json(employees);
});
app.listen(3000,()=> console.log('服務(wù)器已啟動(dòng)'));
我們只需將版本號(hào)添加到端點(diǎn)URL路徑的開頭即可對(duì)其進(jìn)行版本控制。
結(jié)論
設(shè)計(jì)高質(zhì)量REST API的最重要要點(diǎn)是遵循Web標(biāo)準(zhǔn)和約定以保持一致性。 JSON,SSL / TLS和HTTP狀態(tài)代碼都是現(xiàn)代Web的標(biāo)準(zhǔn)構(gòu)建塊。
性能也是重要的考慮因素。 我們可以通過一次不返回太多數(shù)據(jù)來增加它。 另外,我們可以使用緩存,這樣就不必一直查詢數(shù)據(jù)。
端點(diǎn)的路徑應(yīng)一致,我們僅使用名詞,因?yàn)镠TTP方法指示了我們要采取的行動(dòng)。 嵌套資源的路徑應(yīng)位于父資源的路徑之后。 他們應(yīng)該告訴我們我們正在獲取或操作的內(nèi)容,而無需閱讀額外的文檔以了解它的作用。
作者介紹
熱門博客推薦