RESTful API 設(shè)計(jì)最佳實(shí)踐
?點(diǎn)擊上方藍(lán)字關(guān)注我們

簡介
RESTful API 是目前最流行的API設(shè)計(jì)規(guī)范,它用于Web數(shù)據(jù)接口的設(shè)計(jì)。它允許包括瀏覽器在內(nèi)的各種客戶端與服務(wù)器進(jìn)行通信。因此正確設(shè)計(jì)我們的RESTful是相當(dāng)重要的!我們的API必須安全、高性能、同時易于使用。
在本文我們將探討如何設(shè)計(jì)出易于使用并且安全快速的的RESTful API。
RESTful API 即是基于Rest構(gòu)建的API。那么在開始之前,我們先來看看 REST 是什么?
REST與技術(shù)無關(guān),它代表的是一種軟件架構(gòu)風(fēng)格,REST它是 Representational State Transfer 的簡稱,中文的含義是: 表現(xiàn)層狀態(tài)轉(zhuǎn)移(轉(zhuǎn)移:通過HTTP動詞實(shí)現(xiàn))。即 URL 定位資源,HTTP動詞操作(GET,POST,PUT,DELETE)描述操作。
確保接受并響應(yīng) JSON數(shù)據(jù)格式
RESTful API 應(yīng)該接受 JSON 格式的請求,并返回的響應(yīng)體也應(yīng)該是JSON格式的。JSON 是一種數(shù)據(jù)傳輸標(biāo)準(zhǔn),主流編程語言幾乎都能很好的支持它。同時在瀏覽器中我們的JavaScript也能很輕松方便的操作這些數(shù)據(jù)。所以,以JSON 格式編寫的 RESTful API 具有簡單、易讀、易用的特點(diǎn)。
為了確保當(dāng)我們的RESTful API 服務(wù)使用 JSON 格式響應(yīng),我們應(yīng)該將其響應(yīng)頭的Content-Type設(shè)置為application/json。
讓我們來看一個接收 JSON 數(shù)據(jù)并返回 JSON 數(shù)據(jù)的 API 示例。本示例使用Node.js的 Express 框架。我們使用了 body-parser 中間件來解析 JSON 請求體,然后使用 res.json 返回傳入的 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 請求體的字符解析為 JavaScript 對象,然后將其分配給該req.body對象。
在 API 路徑中使用名詞代替動詞
RESTful API是面向資源的API,HTTP動詞操作(GET,POST,PUT,DELETE)描述操作。
我們不應(yīng)該在URL路徑中使用動詞。我們應(yīng)該使用要操作的實(shí)體的名詞作為路徑名。因?yàn)槲覀兊腍TTP請求方法本身就是動詞,就能描述要進(jìn)行的操作,如常見的方法包括 GET,POST,PUT和 DELETE,這些請求方法即可完成 CRUD。
GET 檢索資源。
POST 將新數(shù)據(jù)提交到服務(wù)器。
PUT 更新現(xiàn)有數(shù)據(jù)。
DELETE 刪除數(shù)據(jù)。
例如,我們有個文章(/articles/)資源。我們對其進(jìn)行CRUD的RESTful API如下:
使用 GET /articles/來獲取文章列表使用 POST /articles/添加新文章使用 PUT /articles/:id更新給定ID的文章使用 DELETE /articles/:id刪除具有給定ID的文章
我們通過Express來實(shí)現(xià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('server started'));
在上面的示例代碼中,我們定義了API來操作文章(articles)資源。如我們所見,API URL路徑中使用的都是名詞,作為動詞的請求方法說明了API的操作意圖。
使用名詞復(fù)數(shù)
我們應(yīng)該使用復(fù)數(shù)名詞來命名集合。
通常,我們想要取得的數(shù)據(jù)都是一個集合,而不是單個項(xiàng)目。同時數(shù)據(jù)庫中的表也是具有多個條目的。所以我們的API 也應(yīng)該使用復(fù)數(shù)名詞,這樣更合乎情理。
嵌套分層的資源對象
在處理嵌套資源的API時,應(yīng)該將嵌套資源附加到父資源的路徑之后。
例如一個文章有評論列表,獲取某個文章的評論列表的API則為:
GET /articles/:articleId/comments
我們可以使用express來做個示范:
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('server started'));
使用標(biāo)準(zhǔn)的http狀態(tài)碼
為了消除 API server 發(fā)生錯誤時用戶的困惑,我們應(yīng)該優(yōu)雅地處理錯誤,并返回指示發(fā)生了具體錯誤的HTTP響應(yīng)代碼以及明確的錯誤信息。這可以很好的為API使用者提供了足夠的信息來了解所發(fā)生的問題。
常見的錯誤HTTP狀態(tài)代碼包括:
400錯誤的請求 – 這意味著客戶端輸入驗(yàn)證失敗。401未經(jīng)授權(quán) - 這意味著用戶無權(quán)訪問資源。通常在用戶未通過身份驗(yàn)證時返回。403禁止訪問 - 表示用戶已通過身份驗(yàn)證,但不允許其訪問資源。404Not Found – 表示找不到資源。500內(nèi)部服務(wù)器錯誤 – 這是一般服務(wù)器錯誤。它可能不應(yīng)該明確地拋出。502錯誤的網(wǎng)關(guān) - 這表明來自上游服務(wù)器的無效響應(yīng)。503服務(wù)不可用 – 這表示服務(wù)器端發(fā)生了意外情況(可能是服務(wù)器過載,系統(tǒng)某些部分發(fā)生故障等)。
我們應(yīng)該拋出服務(wù)錯誤相對應(yīng)的錯誤碼。例如,如果我們要拒絕客服端發(fā)起的請求,則應(yīng)在Express API中返回如下所示的 400 響應(yīng):
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
// existing users
const users = [
{ email: '[email protected]' }
]
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('server started'));
在上面的示例中,用戶嘗試創(chuàng)建一個已經(jīng)存在的user,將獲得 400 響應(yīng)狀態(tài)代碼,并帶有一條'User already exists'的錯誤消息,讓用戶知道該用戶已經(jīng)存在。利用這些信息,用戶可以通過使用其他 email 來創(chuàng)建新用戶。
通常錯誤代碼需要附帶明確錯誤消息,以便用戶有足夠的信息來了解自己遇到了什么問題。
每當(dāng)我們的API未成功調(diào)用時,都應(yīng)通過發(fā)送明確的錯誤信息來幫助用戶采取糾正措施來完成操作。
添加過濾,排序和分頁功能
通常我們的數(shù)據(jù)都會非常龐大。我們不可能一次全部返回,這會非常慢也可能導(dǎo)致系統(tǒng)崩潰。因此,我們需要有過濾,分頁數(shù)據(jù)的方式。過濾和分頁都可以通過減少消耗服務(wù)器資源來提高性能。這些功能相當(dāng)基礎(chǔ)且重要。
分頁、過濾、排序查詢都功能都應(yīng)該使用查詢參數(shù)來實(shí)現(xiàn)。如:
/employees?page=1&pageSize=10&firstName=Xing
下面這就是一個帶有過濾查詢的示例:
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('server started'));
保持良好的安全意識
客戶端和服務(wù)器之間的大多數(shù)通信應(yīng)該是私有的。因此,必須使用SSL/TLS進(jìn)行安全保護(hù)。現(xiàn)在加載SSL成本是相當(dāng)?shù)偷摹N覀儧]有理由不使用它。
同時,不同的用戶具有不同的數(shù)據(jù)訪問權(quán)限。例如,普通用戶不應(yīng)該能夠訪問其他用戶的信息。他們也不應(yīng)該能夠訪問管理員的數(shù)據(jù)。
適當(dāng)緩存數(shù)據(jù)以提高性能
可以適當(dāng)添加緩存服務(wù),從緩存中返回常用數(shù)據(jù),而不是每次都從數(shù)據(jù)庫去讀取。緩存的好處是可以更快地獲取數(shù)據(jù),但是也讓我們獲取最新的數(shù)據(jù)變得復(fù)雜。緩存方式有很多如:Redis、內(nèi)存緩存( in-memory cache)等等,我們應(yīng)該根據(jù)自己的應(yīng)用具體情況來選擇是不是該用緩存,使用哪種緩存機(jī)制。
這兒我們來使用Express的apicache中間件來實(shí)現(xià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('server started'));
版本化我們的API
原則上我們應(yīng)該盡量讓API避免破壞性變更,保持向后兼容。但是經(jīng)常有些時候破壞性的變更是不可避免的,這時版本化的API就派上用場了。當(dāng)我們發(fā)布了不兼容或重大更改變,則可以將其發(fā)布在新版本中的API。
我們通常通過URL來實(shí)現(xiàn)版本化,及添加版本號在我們API路徑的開頭,例如:api.liuxing.io/v1 api.liuxing.io/v2
我們可以在 express 很簡單的實(shí)現(xiàn)版本化的RESTful API:
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('server started'));
總結(jié)
設(shè)計(jì)高質(zhì)量RESTful API的最重要的一點(diǎn)是遵循Web標(biāo)準(zhǔn)和約定以保持一致性。JSON、SSL/TLS和HTTP狀態(tài)代碼都是現(xiàn)代Web的標(biāo)準(zhǔn)。性能也是重要的考慮因素。我們可以使用分頁、緩存等手段來提升性能。可維護(hù)性可擴(kuò)展性也是我們需要考慮的。
往期精彩回顧:
這些JavaScript方法將在短短幾分鐘內(nèi)提升你的技能
如何循序漸進(jìn)地學(xué)習(xí)JavaScript?

左手代碼右手磚,拋磚引玉
給點(diǎn)個贊,好不好啊
