Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
npm install -g reveal-md && wget -q -O restful.md https://gist.githubusercontent.com/AlloVince/ba8c33138adbdd39d757/raw/restfulapi.md && reveal-md restful.md

RESTFul API 设计

@AV 2015.04


什么是REST

Representational State Transfer

  • 表现层
  • 状态转化

表现层 = (资源的)表现层

  • 资源是抽象概念,一个资源对应一个URI
  • 表现层是资源的具体形式

状态转化: 客户端通过HTTP协议操作资源,让资源的状态发生变化


什么是RESTFul

符合REST思想的架构

注意: RESTFul仅适用API设计


RESTFul架构的约束

  • Client–server
  • Stateless
  • Cacheable
  • Layered system
  • Code on demand (optional)
  • Uniform interface

Why RESTFul

Imgur

From http://www.programmableweb.com/


举个栗子

CURD

  • GET /tickets - Retrieves a list of tickets
  • GET /tickets/12 - Retrieves a specific ticket
  • POST /tickets - Creates a new ticket
  • PUT /tickets/12 - Updates ticket #12
  • PATCH /tickets/12 - Partially updates ticket #12
  • DELETE /tickets/12 - Deletes ticket #12

基本思路

  • 用名词表示资源
  • 使用Http Methods操作资源
  • 保证GET, PUT, DELETE等操作幂等

HTTP


Send A HTTP Request

telnet www.baidu.com 80
Trying 115.239.210.27...
Connected to www.a.shifen.com.
Escape character is '^]'.
GET / HTTP/1.1
Host: www.baidu.com


Receive A HTTP Response

HTTP/1.1 200 OK
Date: Mon, 18 Apr 2016 04:12:26 GMT
Content-Type: text/html
Content-Length: 14613
Last-Modified: Wed, 03 Sep 2014 02:48:32 GMT
...

<!DOCTYPE html><!--STATUS OK-->
...

Request

POST /v1/oauth2/token HTTP/1.1
Host: api.example.com
Authorization: Basic Y2xpZW50X2lkOmNsaWVudF9zZWNyZXQ=
Content-Type: application/x-www-form-urlencoded

grant_type=password&username=takaaki&password=abcde&scope=api

Response

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache

{
    "access_token": "b77yz37w7kzy8v5fuga6zz93",
    "token_type": "bearer",
    "expires_in": 2629743,
    "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
}

API的设计步骤

  • 公开哪些API
  • Request (Endpoint) 设计
  • Response设计
  • 安全及访问限制

Request 设计


设计原则

以资源为中心

POST /create.do?from=1&to=2&transfer=500.00 👎
POST /transaction http/1.1
host: 127.0.0.1
 
from=1&to=2&amount=500.00 👍

简短

http://api.example.com/service/api/users 👎
http://api.example.com/users 👍

人类可读,符合直觉

http://api.example.com/data/
http://api.example.com/shangpin 👎
http://api.example.com/products/12345 👍

不混用大小写

http://api.example.com/Users/12345
http://example.com/API/getUserName 👎
http://api.example.com/users/12345 👍

建议:大小写敏感,全部采用小写,大写返回404


规则统一

http://api.example.com/friends?id=100
http://api.example.com/friend/100/message 👎
http://api.example.com/friends/100
http://api.example.com/friend/100/message 👍

HTTP Method

  • GET 获取资源
  • POST 创建资源
  • PUT 更新资源
  • DELETE 删除资源
  • PATCH 部分更新资源
  • HEAD 获取资源概要信息
  • OPTIONS 检查资源支持的Method

Method兼容性考量

使用Header覆盖

POST /v1/users/123 HTTP/1.1
Host: api.example.com
X-HTTP-Method-Override: DELETE

使用POST Body覆盖

POST /v1/users/123 HTTP/1.1
Host: api.example.com

user=testuser&_method=PUT

更多细节

统一使用名词复数

http://api.example.com/v1/users/12345

使用常用单词

search👍 find👎
photo👍 picture👎

仅使用英文+数字

http://api.example.com/v1/%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC/123

多个单词使用连字符-

Twitter   /statuses/user_timeline
YouTube   /guideCategories
Facebook  /me/books.quotes
LinkedIn  /v1/people-search
Bit.ly    /v3/user/popular_earned_by_clicks

优先使用资源的子集而非开辟新资源

popular-users 👎
users/popular 👍 

路径 or 参数

  • 是否能完全表示资源
  • 是否可以省略

过滤

GET /tickets
GET /tickets?state=open
GET /tickets?state=open,closed

排序

GET /tickets?sort=-priority
GET /tickets?sort=-priority,created_at 

提交数据

使用JSON


获取关联字段

GET /tickets/12?embed=customer.name,assigned_user
{
  "id" : 12,
  "subject" : "I have a question!",
  "summary" : "Hi, ....",
  "customer" : {
    "name" : "Bob"
  },
  assigned_user: {
   "id" : 42,
   "name" : "Jim",
  }
}

版本区分

理论派

GET /customers/123 HTTP/1.1
Accept: application/vnd.company.myapp.customer-v3+xml

实践派

GET v3.0/customers/123 HTTP/1.1
Accept: application/xml
https://graph.facebook.com/v2.0/me
http://webservices.amazon.com/onca/xml?Service=AWSECommerceService&Version=2011-08-01

可能好的解决方法


Response 设计


数据格式

Twitter    JSON
Facebook   JSON (部分支持XML)
Foursquare JSON
Github     JSON
Amazon     XML
Flickr     XML / JSON
Yahoo      XML / JSON / PHPserialize

指定方式

URL参数

https://api.example.com/v1/users?format=xml

扩展名

https://api.example.com/v1/users.json

Header

GET /v1/users
Host: api.example.com
Accept: application/json

跨域解决方案

JSONP

callback({"id":123,"name":"Saeed"})

CORS

Simple requests

GET /resources/public-data/ HTTP/1.1
Host: bar.other
Origin: http://foo.example
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *

CORS

Preflighted requests

OPTIONS /resources/post-here/ HTTP/1.1
Host: bar.other
Origin: http://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER
Access-Control-Max-Age: 1728000

响应内容设计

支持用户选择字段

http://api.exmample.com/v1/users/12345?fields=name,age

Envelope的必要性

{
  "status": {
    "code": 200,
    "message": 'Success'
  },
  "response": {
     ...results...
  }
}

层次结构的必要性

{
    "id": 23245,
    "name": "Taro Yamada",
    "profile": {
        "birthday": 3456,
        "gender": "male",
        "languages": [ "ja", "en"]
    }
}
{
    "id": 23245,
    "name": "Taro Yamada",
    "birthday": 3456,
    "gender": "male",
    "languages": [ "ja", "en"]
}

数组的处理

[
       {
          "id": 234342,
          "name": "Taro Tanaka",
          "profileIcon": "http://image.example.com/profile/234342.png",
        },
       {
          "id": 93734,
          "name": "Hanako Yamada",
          "profileIcon": "http://image.example.com/profile/93734.png",
       },
       :
]
{
      "friends": [
         {
            "id": 234342,
            "name": "Taro Tanaka",
            "profileIcon": "http://image.example.com/profile/234342.png",
         },
         {
            "id": 93734,
            "name": "Hanako Yamada",
        },
    ]
}

分页

  • page + per_page
  • offset + limit
  • count + since_id + max_id reference

Imgur


分页位置

Github

Link: <https://api.github.com/user/repos?page=3&amp;per_page=100>; rel="next",
      <https://api.github.com/user/repos?page=50&amp;per_page=100>; rel="last"

Google

{
    "nextPageToken": "CKaEL",
    "items": [
        ...
    ]
}

Facebook

{
    "data": [
        ... Endpoint data is here
    ],
    "paging": {
        "cursors": {
            "after": "MTAxNTExOTQ1MjAwNzI5NDE=",
            "before": "NDMyNzQyODI3OTQw"
        },
        "previous": "https://graph.facebook.com/me/albums?limit=25&before=NDMyNzQyODI3OTQw"
            "next": "https://graph.facebook.com/me/albums?limit=25&after=MTAxNTExOTQ1MjAwNzI5NDE="
    }
}

错误处理

HTTP Status Code

1XX 信息
2XX 成功
3XX 重定向
4XX 客户端引起的错误
5XX 服务器端引起的错误

错误信息位置

X-MYNAME-ERROR-CODE: 2013
X-MYNAME-ERROR-MESSAGE: Bad authentication token
X-MYNAME-ERROR-INFO: http://docs.example.com/api/v1/authentication
{
    "error": {
        "code": 2013,
        "message": "Bad authentication token",
        "info": "http://docs.example.com/api/v1/authentication"
    }
}

错误内容

{
   "code" : 1024, //总的错误代码
   "message" : "Validation Failed", //总的错误信息
   "errors": [{
       "developerMessage": "面向开发者的错误信息",
       "userMessage": "面向用户的错误信息(i18n)",
       "code": 2013, //错误代码
       "field" : "first_name", //错误字段
       "info": "http://docs.example.com/api/v1/authentication"
        //参考资料
   }]
}

权限与安全

基于OAuth Token的权限验证

GET /v1/users HTTP/1.1
Host: api.example.com
Authorization: Bearer b77yz37w7kzy8v5fuga6zz93

Token失效

HTTP/1.1 401 Unauthorized
{
    "error":"invalid_token"
}

  • 始终使用SSL
  • HTTPs也需要防范中间人攻击

  • XSS
  • XSRF
  • 大量访问
  • More

速度限制

API形式

GET https://api.github.com/rate_limit
 {
    "resources": {
        "core": {
          "limit": 60,
          "remaining": 60,
          "reset": 1383704430
        },
        "search": {
          "limit": 5,
          "remaining": 5,
          "reset": 1383700890
        }
	}, "rate": {
        "limit": 60,
        "remaining": 60,
        "reset": 1383704430
	}
}

HTTP Header形式

  • X-RateLimit-Limit 单位时间的访问数上限
  • X-RateLimit-Remaining 剩余访问数
  • X-RateLimit-Reset 访问数重置的时间

超出访问数

HTTP/1.1 429 Too Many Requests
Content-Type: text/html
Retry-After: 3600


{
   "errors": [
        {
          "code": 88,
          "message": "Rate limit exceeded"
        }
  ]
}

综合实例

GET https://api.github.com/users/AlloVince
HTTP/1.1 200 OK
Server: GitHub.com

Date: Mon, 16 Jun 2014 21:32:36 GMT
Content-Type: application/json; charset=utf-8
Status: 200 OK
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 55
X-RateLimit-Reset: 1402957018
X-XSS-Protection: 1; mode=block
X-Frame-Options: deny
Content-Security-Policy: default-src 'none'
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: ETag, Link, X-GitHub-OTP, X-RateLimit- Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval
Access-Control-Allow-Origin: *
X-GitHub-Request-Id: 719794F7:01FB:299F044:539F6273 Strict-Transport-Security: max-age=31536000
X-Content-Type-Options: nosniff


面向未来


REST Levels

  • REST LEVEL0 - 使用HTTP
  • REST LEVEL1 - 引入资源的概念
  • REST LEVEL2 - 引入HTTP动词(GET/POST/PUT/DELETE)
  • REST LEVEL3 - 引入HATEOAS

Refer: Richardson Maturity Model


HATEOAS

超媒体即引用状态引擎(Hypermedia As The Engine Of Application State)

[
{
    "href": "https://api.sandbox.paypal.com/v1/payments/payment/PAY-6RV70583SB702805EKEYSZ6Y",
        "rel": "self",
        "method": "GET"
},
{
    "href": "https://www.sandbox.paypal.com/webscr?cmd=_express-checkout&token=EC-60U79048BN7719609",
    "rel": "approval_url",
    "method": "REDIRECT"
},
{
    "href": "https://api.sandbox.paypal.com/v1/payments/payment/PAY-6RV70583SB702805EKEYSZ6Y/execute",
    "rel": "execute",
    "method": "POST"
}
]

可能关联的行为

  • self
  • parent_payment
  • sale
  • update
  • authorization
  • reauthorize
  • capture
  • void
  • refund
  • delete

API的用户是谁

  • LSUDs (large set of unknown developers)
  • SSKDs (small set of known developers)

REST并不能解决所有问题

  • 并不是所有业务都能抽象为资源
  • 性能 (1 Screen 1 Call)

需要反思的问题

  • 这样设计是不是只为了保持Model字段的纯净
  • 这样设计是不是只为了好维护

Expend

OData(Open Data Protocol), by Microsoft

GET http://services.odata.org/Northwind/Northwind.svc/Products?$format=json
GET http://services.odata.org/Northwind/Northwind.svc/Products?$format=json&$expand=Supplier

More expend example

GET /authors?name=Tol*
GET /authors?name=Tol*&expand=books,books.reviews,books.sales,bioData

DSL (YQL)

USE "http://myserver.com/mytables1.xml" as table1;
USE "http://myserver.com/mytables2.xml" as table2;
SELECT * FROM table1 WHERE id IN (select id FROM table2)

https://developer.yahoo.com/yql/


Orchestration Layer

Netflix

http://techblog.netflix.com/2013/01/optimizing-netflix-api.html

http://www.redotheweb.com/2012/08/09/how-to-design-rest-apis-for-mobile.html


Reference

RESTFul Web API


Thank you


View this slide locally

brew install npm
npm install -g reveal-md
wget https://gist.githubusercontent.com/AlloVince/ba8c33138adbdd39d757/raw/gistfile1.md
reveal-md gistfile1.md -s "^\n---"  -v "^\n----" -t "league"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.