HTTP API Design

Presented by @tlhunter

Advanced Microservices: thomashunter.name/ms

  • Section 1: Requests
  • Section 2: Responses
  • Section 3: Bodies
  • Section 4: API Standards

Section 1:

Requests

HTTP Request Overview

POST /v1/animal HTTP/1.1\n        <-- Request Line
Host: api.example.org\n           <-- Request Headers
Accept: application/json\n
Content-Type: application/json\n
Content-Length: 24\n
\n                                <-- Two Newlines
{\n                               <-- Body
  "name": "Gir",\n
  "animal_type": "12"\n
}\n

Endpoints (Paths)

  • Represent data as collections of resources
  • Represent all actions as CRUD operations
  • /collection
  • /collection/{resource_id}
  • /collection/{resource_id}/sub
  • /collection/{resource_id}/sub/{id}

GET Method

  • Read resource or resources
  • Safe: doesn't alter state
  • Idempotent: repeatable
  • Shouldn't have a body
  • GET /collection?filters
  • GET /collection/{resource_id}?fields

POST Method

  • Create resource
  • Unsafe: alters state
  • Not Idempotent: repeating will create new resource
  • Should have a body
  • POST /collection

DELETE Method

  • Destroys resource
  • Unsafe: alters state
  • Idempotent: repeating has no side effect
  • Shouldn't have a body
  • DELETE /collection/{resource_id}

PUT/PATCH Methods

  • Update resource
  • Unsafe: alters state
  • Idempotent: repeating has no side effect
  • Should have a body
  • PUT /collection/{resource_id}
    • Replace entire resource
  • PATCH /collection/{resource_id}
    • Replace partial resource

HEAD/OPTIONS Methods

  • Safe: doesn't alter state
  • Idempotent: repeatable
  • Shouldn't have a body
  • HEAD /endpoint
    • Retrieve only the headers for a resource
  • OPTIONS /endpoint
    • Retrieve consumer capabilities with resource
    • Required by modern browseres and CORS

Request Headers

  • Accept: Content Types the client accepts
  • Accept-Language: Language expected by client
  • Content-Length: Length of request body
  • Content-Type: Type of data if a body is present
  • Host: For virtual hosting, usually ignored by app
  • User-Agent: Information about client

Section 2:

Responses

HTTP Response Overview

HTTP/1.1 200 OK\n                       <-- Status Line
Date: Wed, 14 Jun 2017 23:23:01 GMT\n   <-- Response Headers
Content-Type: application/json\n
Access-Control-Max-Age: 1728000\n
Cache-Control: no-cache\n
\n                                      <-- Two Newlines
{\n                                     <-- Body
  "id": "12",\n
  "created": "2017-06-14T23:22:59Z",\n
  "modified": null,\n
  "name": "Gir",\n
  "animal_type": "12"\n
}\n

1XX Status Codes - Informational

  • You will probably never use this
  • 101 Switching Protocols

2XX Status Codes - Successful

  • 200 OK
  • 201 Created
  • 202 Accepted (e.g. asynchronous creation)
  • 204 No Content

3XX Status Codes - Redirection

  • 301 Moved Permanently
  • 302 Found
  • 307 Temporary Redirect
  • 308 Permanent Redirect

4XX Status Codes - Client Error

  • Server state should not have been altered
  • 400 Invalid Request
  • 401 Unauthorized
  • 403 Forbidden
  • 404 Not Found
  • 405 Method Not Allowed
  • 406 Not Acceptable

5XX Status Codes - Server Error

  • All Unsafe requests leave server in unknown state
  • 500 Internal Server Error
  • 501 Not Implemented
  • 503 Service Unavailable (e.g. no database)
  • 521 Web Server Is Down

Response Headers

  • Cache-Control: Cache policy, e.g. no-cache
  • Content-Language: E.g. en-US
  • Content-Length: Response body length in bytes
  • Content-Type: E.g. application/json
  • Date: Date and time on the server
  • Expires: Time when content should expire
  • Server: Useless field for bragging

Section 3:

Bodies

JSON (JavaScript Object Notation)

  • Preferred serialization format for most popular APIs
  • Terse format which easily serializes/deserializes
{
  "strings": "value",
  "numbers": 12,
  "moreNumbers": -0.1,
  "booleans": true,
  "nullable": null,
  "array": [1, "abc", false],
  "object": { "very": "deep" }
}

Attribute Name Casing

  • snake_case: The most bytes
  • PascalCase: The most shifts
  • camelCase: Happy medium
  • Doesn't matter what you pick, but be consistent
  • Pro Tip: API doesn't need to match internal data

Booleans

  • Use positive, happy words
    • Easier to remember, less glancing at docs
    • enabled instead of disabled
    • public instead of private
  • No is_cool or cool_flag, just cool

Timestamps

  • Always use ISO 8601
    • "2017-06-15T04:23:46+00:00"
    • "2017-06-15T04:23:46Z"
    • "2017-06-15T04:23:46.987Z"
  • Unix Epoch is unreadable and ambiguous, e.g.
    1493268311123

Identifiers

  • Always transmit strings, never raw integer
    • “32-bit iOS devices are experiencing issues due to limitations interpreting game IDs over 2,147,483,647. Fix should be out in 48 hours :)”
      -- @chesscom
  • Two types of identifiers, choose based on need
    • Incremental (Integer, Base62): Efficient
    • Random (UUID): Cannot guess ID's, count

Versioning

  • URL (LinkedIn, Google+, Twitter):
    • https://api.example.org/v1/*
  • Accept Header (GitHub):
    • Accept: application/json+v1
  • Custom Header (Joyent CloudAPI):
    • X-Api-Version: 1
  • Backward-breaking changes require new version
  • Deprecate old versions with an upgrade deadline

Section 4:

API Standards

Simple Envelope

  • Provide standardized metadata about response
{
  "error": "database_connection_failed",
  "error_human": "Unable to establish database connection",
  "data": null
}
{
  "error": null,
  "error_human": null,
  "data": [{"id": "11"}, {"id": "12"}],
  "offset": 10,
  "per_page": 10
}

JSON API

  • Reference other resources without redundancies
{
  "data": [{
    "type": "articles", "id": "1",
    "attributes": {
      "title": "Article Title", "body": "Content"
    },
    "relationships": {
      "author": { "data": {"id": "42", "type": "people"} }
    }
  }],
  "included": [{
    "type": "people", "id": "42",
    "attributes": { "name": "John", "age": 80 }
  }]
}

GraphQL (Request)

  • Specify desired fields, great for facade's
{
  user(id: "tlhunter") {
    id
    name
    photo {
      id
      url
    }
    friends {
      id
      name
    }
  }
}

GraphQL (Response)

  • Data returned as JSON from multiple services
{
  "data": {
    "user": {
      "name": "Thomas Hunter II",
      "id": "tlhunter",
      "photo": { "id": "12", "url": "http://im.io/12.jpg" },
      "friends": [
        { "name": "Rupert Styx", "id": "rupertstyx" }
      ]
    }
  }
}

MessagePack

  • Binary representation of JSON
  • JSON (31 bytes, w/o whitespace)
{
  "id": "tlhunter",
  "xyz": [1,2,3]
}
  • Corresponding MessagePack (21 bytes)
82 a2 69 64 a8 74 6c 68 75 6e 74
65 72 a3 78 79 7a 93 01 02 03

JSON RPC

  • Request
{"jsonrpc":"2.0","method":"subtract","params":[42,23],"id":1}
  • Response
{"jsonrpc": "2.0", "result": 19, "id": 1}
  • No endpoint/CRUD abstractions like with HTTP
  • RPC happily exists outside of HTTP (TCP, IPC)
  • If over HTTP, probably single endpoint w/ POST

Questions?