2015年9月14日

Rest API documentation: Swagger, RAML, API Blueprint

以往在製作網路服務 web service 時,通常會率先想到 WSDL,但在 REST 以及輕量化網路傳輸的概念普及後,大家已經逐漸放棄 XML 這個冗長的描述文件格式,轉向使用 JSON,並用 HTTP REST 的方式提供網路服務 API。至於 API 文件規範,目前比較常見的有 Swagger, RAML, API Blueprint 三種,三種規範使用的技術背景不同,呈現出來的文件樣式差異也很大。

REST API

WSDL 一開始就是從規格開始做起,大家習慣 WSDL 的實作,必須要先定義 WSDL 描述檔,然後透過不同語言的 server 及 stub 的程式產生工具,產生對應的程式碼,在分別去實做 server 與 client side 的程式。

然而 REST 的概念是由技術概念開始推廣的,技術上強調我們平常都只有使用到 HTTP 規範中的 GET 與 POST method,但其實還有 PUT DELETE,這四個 http method 正好可以對應到一個資源的 CRUD operation,再加上 JSON 這個簡潔的資源表示方式的流行,這讓大家接受 REST API,而 WSDL 逐漸被淘汰。

REST 的規格,其實就是 HTTP,因此 REST API 就沒有什麼特別的規範,大家也都是直接撰寫 API,至於文件的部份,每一個人都有自己的文件規格與寫法,這跟 WSDL 的實做方式完全不同。

換句話說,想到 WSDL 就會想到要先定義 API 的規格,才能繼續往下工作,而想到 REST API,就會直覺想到 agile,反正程式先寫了再說。

Rest API Spec

API 是要給其他程式設計師使用的,因此就必須要有對應的文件,描述 API 的內容、用途,並提供範例。在沒有 WSDL Spec 的共通標準之下,後來才逐漸有了標準化 API 的想法,Swagger 是目前討論最多、也發展最久的 API Spec,對應可以使用的工具也很多。

Another API-Blueprint, RAML and Swagger Comparison 提供了Swagger, RAML, API Blueprint 三種 API Spec 的比較表,以下節錄一小部份的內容。

Specs API Blueprint RAML Swagger
Format Markdown YAML JSON
License MIT ASL 2.0 ASL 2.0
Version 1A8 0.8 1.2
Support Apiary Mulesoft Reverb
Initial Commit 2013/4 2013/9 2011/7
Web Site https://apiblueprint.org http://raml.org http://swagger.io/
API design approach top-down top-down bottom-up

Hello World Product API With Blueprint, RAML And Swagger 這篇文章以實際的例子告訴大家,這三種規格的差異在哪裡,API Blurprint 以 Markdown 語法撰寫主要是用來閱讀的,swagger 很明顯地是想要取代 WSDL,希望大家能夠先撰寫 swagger API 定義,然後用工具產生程式碼,而 RAML 比較想取得中庸的位置,既有閱讀性,又能夠讓程式閱讀,產生一些對應的工具。

API Blueprint

# Group Product
Resources for working with a single product.

## Product [/products/{id}]
Provides access to a single product.

+ Parameters
    + id (string) ... ID of the product

+ Model (application/json)

    JSON representation of products.

    + Body

            {
            "id": "1",
            "name": "Product One",
            "description": "This is the full description of the product.",
            "url": "http://example.com",
            "image": "http://example.com/image.jpg"
            }

RAML

#%RAML 0.8
---
#===============================================================
#  Products API - RAML example
#  References:
#    - RAML Specification - http://raml.org/spec.html
#    - RAML Projects - http://raml.org/projects.html
#    - RAML Tools  - http://www.apihub.com/raml-tools
#===============================================================
title: Products API
version: v0.1
baseUri: 
#List of media type to support
mediaType:  application/json
#List of protocols to support for baseUri
protocols: [ HTTP, HTTPS ]

#===============================================================
#  API documentation
#===============================================================
documentation:
  - title: Home
    content: | #This is a prototype product API.
  - title: Getting Started
    content: TODO   

#===============================================================
# API resource definitions
#===============================================================                        

/products:
  uriParameters:
  displayName: Products
  description: A collection of products
  post:
    description: Create a product
    #Post body media type support
    #text/xml: !!null  # media type text, xml support
    #application/json: !!null  #media type json support
    body:
      application/json:
        schema: |
          {
            "$schema": "http://json-schema.org/draft-03/schema",
            "product": {
                "name": {
                    "required": true,
                    "type": "string"
                },
                "description": {
                    "required": true,
                    "type": "string"
                },                
                "url": {
                    "required": true,
                    "type": "string"
                },                
                "image": {
                    "required": true,
                    "type": "string"
                }
            },
            "required": true,
            "type": "object"
          }        
        example: |
          {
            "product": {
              "id": "1",
              "name": "Product One",
              "description": "This is the full description of the product.",
              "url": "http://example.com",
              "image": "http://example.com/image.jpg"
            }
          }
  get:
    description: Get a list of products
    queryParameters:
      q:
        description: Search phrase to look for products
        type: string
        required: false
    responses:
      200:
        body:
          application/json: 
            #example: !include schema/product-list.json

  #---------------------------------------------------------------
  # Nested resource representing a  single product - name parameter as part of the path.
  #---------------------------------------------------------------
  /{productId}: 
    description: | # Retrieve a specific product using its ID.
    uriParameters:
     productId:
       displayName: Product ID
       type: integer    
    get:
      description: Get a single product
      queryParameters:
        productId:
          description: The ID of the product
          type: integer
          required: true
      responses:
        200:
          body:
            application/json: 
              #example: !include schema/product-list.json

swagger

{
    "apiVersion": "1.0",
    "swaggerVersion": "1.2",
    "basePath": "",
    "resourcePath": "/products",
    "produces": [
        "application/json"
    ],
    "apis": [
        {
            "path": "/products/",
            "operations": [
                {
                    "method": "GET",
                    "summary": "Pulls a listing of products",
                    "notes": "Returns a list of products, allowing you to filter by keyword query.",
                    "nickname": "getJobs",
                    "type": "products",
                    "parameters": [
                        {
                            "name": "query",
                            "description": "a text query to search across products",
                            "required": false,
                            "allowMultiple": false,
                            "dataType": "string",
                            "paramType": "query"
                        }
                    ],
                    "produces": [
                        "application/json"
                    ],
                    "responseMessages": [
                        {
                            "code": 404,
                            "message": "No Jobs To Return"
                        }
                    ]
                }
            ]
        },
        {
            "path": "/products/{id}",
            "operations": [
                {
                    "method": "GET",
                    "summary": "Retrieve an product using its ID",
                    "notes": "Returns a products detail",
                    "type": "people",
                    "nickname": "getJobs",
                    "produces": [
                        "application/json"
                    ],
                    "parameters": [
                        {
                            "name": "id",
                            "description": "id for the video, notice this is in the path, not a query variable",
                            "required": false,
                            "allowMultiple": false,
                            "dataType": "integer",
                            "paramType": "path"
                        }
                    ],
                    "responseMessages": [
                        {
                            "code": 400,
                            "message": "Invalid Application Information ID supplied"
                        },
                        {
                            "code": 404,
                            "message": "People not found"
                        }
                    ]
                }
            ]
        }
    ],
    "models": {
        "products": {
            "id": "products",
            "properties": {
                "id": {
                    "type": "integer"
                },              
                "name": {
                    "type": "string"
                },
                "description": {
                    "type": "string"
                },
                "url": {
                    "type": "string"
                },
                "video": {
                    "type": "string"
                },
                "thumbnailUrl": {
                    "type": "string"
                },
                "keywords": {
                    "type": "string"
                },
                "inLanguage": {
                    "type": "string"
                },
                "creator": {
                    "type": "string"
                }
            }
        }
    }
}

用途決定使用方法

不同的文件規範,加上支援的工具多寡,可以決定這份規格的影響範圍,也就等於告訴我們該選擇那一種規格。

swgger 是目前討論最多的,原因在於這是最早推出的規格,有 UI, editor, SDK generator 等等完整的工具可以使用,但是因為這是使用 JSON 的格式,脫離了工具的優勢,文件本身的可讀性就會差了一些,畢竟這是要給程式碼閱讀的一種規格。

另外 swagger 的 bottom-up 開發方式,也比較不那麼直覺,一般人在描述事情時,會習慣先從大維度的方向說起,然後再一步一步說明細節,而 swagger 為了減少文件 parsing 的次數,必須以 bottom-up 的方式描述規格的內容。

因此 RAML 與 API Blueprint 都是用 top-down 的方法,而且選擇了比較適合直接閱讀的文件格式。

不遵循規格也不會有問題

規格是為了互通以及共通的工具而定義出來的,REST API 並不像是 WSDL,缺少了 API 的定義文件,就沒辦法繼續進行程式開發,很多時候都是以先做出來為出發點,這時候也不會考慮什麼文件的規格,直接用文字檔案的方式,直接寫一寫 API 簡述,就可以馬上進入 coding。

在資料互動與專案成員之間的溝通方式中,如果已經有了既有的習慣跟方法,那麼沿用自己團隊的方法也沒關係,但如果我們需要對外發布 API Spec,或許就需要選擇一種規格與方法,讓團隊以外的人,能遵循該規格的處理方式,以最舒服且方便的方式,取得文件的說明,甚至是直接產生 API 的 stub code。