RESTful API設計原則與規范
一、背景與基礎概念 2
二、RESTful API應遵循的原則 3
1、協議(Protocol) 3
2、域名(ROOT URL) 3
3、版本(Versioning) 3
4、路徑(Endpoints) 3
5、HTTP動詞(HTTP Verbs) 4
6、過濾信息(Filtering) 5
7、狀態碼(Status Codes) 5
8、錯誤處理(Error handling) 6
9、返回結果(Response) 6
10、使用HATEOAS的Hypermedia API 6
11、認證(Authentication) 7
三、Swagger API標准 7
REST,即Representational State Transfer的縮寫。RESTful架構,是目前最流行的一種互聯網軟件架構。它結構清晰、符合標准、易於理解、擴展方便,基於這個風格設計的軟件可以更簡潔,更有層次,更易於實現緩存等機制,所以正得到越來越多網站的采用。如果一個架構符合REST原則,就稱它為RESTful架構。
本文即將描述的,即是創建RESTful架構的API所要遵循的原則與規范。
一、背景與基礎概念
Web 應用程序最重要的 REST 原則是,客戶端和服務器之間的交互在請求之間是無狀態的。從客戶端到服務器的每個請求都必須包含理解請求所必需的信息。
- 資源(resource):網絡上的一個實體或者說是一個具體信息,可以是一段文本、一張圖片、一首歌曲、一種服務。
- 統一資源定位符(URI,Universal Resource Identifier):一個資源的識別符或者說是一個地址,通過URI你可以定位到特定的資源。要獲取這個資源,需要訪問它的URI,因此,URI就成了每一個資源的地址或獨一無二的識別符。
- 狀態轉換(State Transfer): 所有資源都共享統一的接口,以便在客戶端和服務器之間傳輸狀態。客戶端與服務器互動的過程,通常涉及到服務器端數據和狀態的變化過程,比如文件被修改,訪問數量增加等。使用的是標准的 HTTP 方法,Http標准中定義的最主要四個動詞:GET、POST、PUT、DELETE。它們分別對應四種基本操作:
- GET: 用來獲取資源
- POST: 用來新建資源
- PUT: 用來更新資源
- DELETE: 用來刪除資源
- Hypermedia 是應用程序狀態的引擎,資源表示通過超鏈接互聯。
二、RESTful API應遵循的原則
1、協議(Protocol)
API與用戶的通信協議,盡量使用HTTPs協議。HTTPs協議的所有信息都是加密傳播,第三方無法竊聽,具有校驗機制,一旦被篡改,通信雙方會立刻發現,配備身份證書,防止身份被冒充。
2、域名(ROOT URL)
應該盡量將API部署在專用域名之下。
https://api.example.com
如果確定API很簡單,不會有進一步擴展,可以考慮放在主域名下。
https://example.org/api/
3、版本(Versioning)
應該將API的版本號放入URL。
https://api.example.com/v1/
另一種做法是,將版本號放在HTTP頭信息中,但不如放入URL方便和直觀。Github采用這種做法。
注:需要注意版本規划,以便以后API的升級和維護。使得API版本變得強制性,不要發布無版本的API,使用簡單數字,避免小數點如2.5。
4、路徑(Endpoints)
路徑表示API的具體網址URL。在RESTful架構中,每個URL代表一種資源(resource),所以網址中不能有動詞,只能有名詞,而且所用的名詞往往與代表的對象名稱對應。一般來說,某一同種記錄的”集合”(collection),所以API中的名詞也應該使用復數。
具體細則:
1、使用名詞而不是動詞。舉例來說,某個URL是/cards/show/1,其中show是動詞,這個URL就設計錯了,正確的寫法應該是/cards/1,然后用GET方法表示show。如果某些動作是HTTP動詞表示不了的,你就應該把動作做成一種資源。比如網上匯款,從賬戶1向賬戶2匯款500元,錯誤的URI是:POST /accounts/1/transfer/500/to/2。正確的寫法是把動詞transfer改成名詞transaction,資源不能是動詞,但是可以是一種服務:POST /transaction?from=1&to=2&amount=500.00。
2、使用復數名詞。不要混淆名詞單數和復數,為了保持簡單,只對所有資源使用復數。
舉例:
/cars 而不是 /car
/users 而不是 /user
/products 而不是 /product
/settings 而不是 /setting
3、使用子資源表達關系。如果一個資源與另外一個資源有關系,使用子資源。
舉例:
GET /cars/911/drivers/ 返回 car 911的所有司機
GET /cars/911/drivers/8 返回 car 911的8號司機
5、HTTP動詞(HTTP Verbs)
對於資源的具體操作類型,由HTTP動詞表示。常用的HTTP動詞有下面五個:
- GET(SELECT):從服務器取出資源(一項或多項)。
- POST(CREATE):在服務器新建一個資源。
- PUT(UPDATE):在服務器更新資源(客戶端提供改變后的完整資源)。
- PATCH(UPDATE):在服務器更新資源(客戶端提供改變的屬性)。
- DELETE(DELETE):從服務器刪除資源。
還有兩個不常用的HTTP動詞。
- HEAD:獲取資源的元數據。
- OPTIONS:獲取信息,關於資源的哪些屬性是客戶端可以改變的。
注:Get方法和查詢參數不應該涉及狀態改變。使用PUT, POST 和DELETE方法而不是 GET 方法來改變狀態。
6、過濾信息(Filtering)
如果記錄數量很多,服務器不可能都將它們返回給用戶。API應該提供參數,過濾返回結果。為集合提供過濾、排序、選擇和分頁等功能。
下面是一些常見的參數。
- ?limit=10:指定返回記錄的數量
- ?offset=10:指定返回記錄的開始位置。
- ?pageNumber=2&perSize=100:指定第幾頁,以及每頁的記錄數。
- ?sortby=name&order=asc:指定返回結果按照哪個屬性排序,以及排序順序。
- ?animal_type_id=1:指定篩選條件
參數的設計允許存在冗余,即允許API路徑和URL參數偶爾有重復。比如,GET /zoo/ID/animals 與 GET /animals?zoo_id=ID 的含義是相同的
注:
①移動端能夠顯示其中一些字段,它們其實不需要一個資源的所有字段,給API消費者一個選擇字段的能力,這會降低網絡流量,提高API可用性。
②為了將總數發給客戶端,使用訂制的HTTP頭: X-Total-Count.
7、狀態碼(Status Codes)
服務器向用戶返回的狀態碼和提示信息,常見的有以下一些(方括號中是該狀態碼對應的HTTP動詞)。
- 200 OK - [GET]:服務器成功返回用戶請求的數據,該操作是冪等的(Idempotent)。
- 201 CREATED - [POST/PUT/PATCH]:用戶新建或修改數據成功。
- 202 Accepted - [*]:表示一個請求已經進入后台排隊(異步任務)
- 204 NO CONTENT - [DELETE]:用戶刪除數據成功。
- 400 INVALID REQUEST - [POST/PUT/PATCH]:用戶發出的請求有錯誤,服務器沒有進行新建或修改數據的操作,該操作是冪等的。
- 401 Unauthorized - [*]:表示用戶沒有權限(令牌、用戶名、密碼錯誤)。
- 403 Forbidden - [*]:表示用戶得到授權(與401錯誤相對),但是訪問是被禁止的。
- 404 NOT FOUND - [*]:用戶發出的請求針對的是不存在的記錄,服務器沒有進行操作,該操作是冪等的。
- 406 Not Acceptable - [GET]:用戶請求的格式不可得(比如用戶請求JSON格式,但是只有XML格式)。
- 410 Gone -[GET]:用戶請求的資源被永久刪除,且不會再得到的。
- 422 Unprocesable entity - [POST/PUT/PATCH]:當創建一個對象時,發生一個驗證錯誤。
- 500 INTERNAL SERVER ERROR - [*]:服務器發生錯誤,用戶將無法判斷發出的請求是否成功。
8、錯誤處理(Error handling)
如果狀態碼是4xx,就應該向用戶返回出錯信息。盡量使用詳細的錯誤包裝信息:
{
"errors": [
{
"userMessage": "Sorry, the requested resource does not exist",
"internalMessage": "No car found in the database",
"code": 4xx,
"more info": "http://dev.example.com/api/v1/errors/12345"
}
]
}
9、返回結果(Response)
服務器返回的數據格式,應該盡量使用JSON,避免使用XML。針對不同操作,服務器向用戶返回的結果應該符合以下規范。
- GET /collection:返回資源對象的列表(數組)
- GET /collection/resource:返回單個資源對象
- POST /collection:返回新生成的資源對象
- PUT /collection/resource:返回完整的資源對象
- PATCH /collection/resource:返回完整的資源對象
- DELETE /collection/resource:返回一個空文檔
10、使用HATEOAS的Hypermedia API
RESTful API最好使用Hypermedia as the Engine of Application State(超媒體作為應用狀態的引擎),即返回結果中提供鏈接,連向其他API方法,超文本鏈接可以建立更好的文本瀏覽,使得用戶不查文檔,也知道下一步應該做什么。
比如,當用戶向api.example.com的根目錄發出請求,會得到這樣一個文檔。
{"link": {
"rel": "collection https://www.example.com/zoos",
"href": "https://api.example.com/zoos",
"title": "List of zoos",
"type": "application/vnd.yourformat+json"
}}
上面代碼表示,文檔中有一個link屬性,用戶讀取這個屬性就知道下一步該調用什么API了。rel表示這個API與當前網址的關系(collection關系,並給出該collection的網址),href表示API的路徑,title表示API的標題,type表示返回類型。
11、認證(Authentication)
API的身份認證盡量使用OAuth 2.0框架。
三、Swagger API標准
API的文檔管理和信息描述,將使用Swagger進行。
Swagger是一個規范和完整的框架,用於生成、描述、調用和可視化RESTful風格的Web服務。Swagger的目標是對REST API定義一個標准的和語言無關的接口,可讓人和計算機無需訪問源碼、文檔或網絡流量監測就可以發現和理解服務的能力。
Swagger規范定義了一組描述一個API所需的文件格式,類似於描述Web服務的WSDL。通過Swagger進行REST API的正確定義,用戶可以理解遠程服務並使用最少實現邏輯與遠程服務進行交互。與為底層編程所實現的接口類似,Swagger消除了調用服務時可能會有的猜測。
注:Microsoft,PayPal等公司也已經引入Swagger 來定義自己的REST API 文檔。
Swagger API可以使用YAML或JSON來表示。
Swagger這類API文檔工具可以滿足下列需求:
- 支持API自動生成同步的在線文檔
- 這些文檔可用於項目內部API審核
- 方便測試人員了解API
- 這些文檔可作為客戶產品文檔的一部分進行發布
- 支持API規范生成代碼,生成的客戶端和服務器端骨架代碼可以加速開發和測試速度
通常情況下,API的Swagger描述為JSON文件,也可使用YAML描述的文件。
附Swagger文件示例:
{
"swagger": "2.0",
"info": {
"version": "1.0.0",
"title": "Swagger Petstore (Simple)",
"description": "A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification",
"termsOfService": "http://helloreverb.com/terms/",
"contact": {
"name": "Swagger API team",
"email": "foo@example.com",
"url": "http://swagger.io"
},
"license": {
"name": "MIT",
"url": "http://opensource.org/licenses/MIT"
}
},
"host": "petstore.swagger.io",
"basePath": "/api",
"schemes": [
"http"
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"paths": {
"/pets": {
"get": {
"description": "Returns all pets from the system that the user has access to",
"operationId": "findPets",
"produces": [
"application/json",
"application/xml",
"text/xml",
"text/html"
],
"parameters": [
{
"name": "tags",
"in": "query",
"description": "tags to filter by",
"required": false,
"type": "array",
"items": {
"type": "string"
},
"collectionFormat": "csv"
},
{
"name": "limit",
"in": "query",
"description": "maximum number of results to return",
"required": false,
"type": "integer",
"format": "int32"
}
],
"responses": {
"200": {
"description": "pet response",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/pet"
}
}
},
"default": {
"description": "unexpected error",
"schema": {
"$ref": "#/definitions/errorModel"
}
}
}
},
"post": {
"description": "Creates a new pet in the store. Duplicates are allowed",
"operationId": "addPet",
"produces": [
"application/json"
],
"parameters": [
{
"name": "pet",
"in": "body",
"description": "Pet to add to the store",
"required": true,
"schema": {
"$ref": "#/definitions/newPet"
}
}
],
"responses": {
"200": {
"description": "pet response",
"schema": {
"$ref": "#/definitions/pet"
}
},
"default": {
"description": "unexpected error",
"schema": {
"$ref": "#/definitions/errorModel"
}
}
}
}
},
"/pets/{id}": {
"get": {
"description": "Returns a user based on a single ID, if the user does not have access to the pet",
"operationId": "findPetById",
"produces": [
"application/json",
"application/xml",
"text/xml",
"text/html"
],
"parameters": [
{
"name": "id",
"in": "path",
"description": "ID of pet to fetch",
"required": true,
"type": "integer",
"format": "int64"
}
],
"responses": {
"200": {
"description": "pet response",
"schema": {
"$ref": "#/definitions/pet"
}
},
"default": {
"description": "unexpected error",
"schema": {
"$ref": "#/definitions/errorModel"
}
}
}
},
"delete": {
"description": "deletes a single pet based on the ID supplied",
"operationId": "deletePet",
"parameters": [
{
"name": "id",
"in": "path",
"description": "ID of pet to delete",
"required": true,
"type": "integer",
"format": "int64"
}
],
"responses": {
"204": {
"description": "pet deleted"
},
"default": {
"description": "unexpected error",
"schema": {
"$ref": "#/definitions/errorModel"
}
}
}
}
}
},
"definitions": {
"pet": {
"type": "object",
"required": [
"id",
"name"
],
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"name": {
"type": "string"
},
"tag": {
"type": "string"
}
}
},
"newPet": {
"type": "object",
"required": [
"name"
],
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"name": {
"type": "string"
},
"tag": {
"type": "string"
}
}
},
"errorModel": {
"type": "object",
"required": [
"code",
"message"
],
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"message": {
"type": "string"
}
}
}
}
}
Swagger文件的構成以及規范信息,在Swagger官方的規范指導《Swagger RESTful API文檔規范》中有詳細描述,請參看。
http://swagger.io/specification/