Building "cooperative" APIs with HTTP/2

Gabe Sullice

 

Senior Engineer - Acquia

 

JSON API co-maintainer

HTTP/2 enthusiast

Specification-junkie

Loses lots of sleep over API design

HTTP
TCP
IP

IP

Internet Protocol

IP has the task of delivering packets from the source host to the destination host solely based on the IP addresses in the packet headers.

TCP

Transport Control Protocol

TCP provides reliable, ordered, and error-checked delivery of a stream of octets (bytes) between applications running on hosts communicating by an IP network.

HTTP

Hypertext transfer protocol

HTTP is an application protocol for distributed, collaborative, and hypermedia information systems.

HTTP/1.1

An application language

URis, METHODS, and messages 

  • Resource - A unit of information
  • URI - Uniform Resource Identifier
  • Method - GET, POST, PATCH, DELETE & others
  • Status - 200, 300, 400, 500 inter alia
  • Messages - "Requests" and "Responses"

URI

UNiform resource Identifier

Almost always a URL

Which is simply a unique identifier that can be "located"

http://example.com/api/user/1

 

"http:" "//" host [ ":" port ] [ abs_path [ "?" query ]]

Methods

GET, POST, PATCH, PUT & DELETE

Describe "actions" on a resource

HEAD & OPTIONS are about information discovery

GET - Show me the identified resource

POST - Add this resource

PATCH - Update the identified resource with this information

PUT - Replace the identified resource with this one

DELETE - Remove the identified resource

HEAD - Is there a resource with this identifier?

OPTIONS - What actions can I take on a resource?

Statuses

200, 300, 400, 500

Categorize communication into conversational buckets

200 - Understood

300 - Maybe you meant X?

400 - Sorry, I don't understand

500 - It's not you, it's me

Messages

requests and responses

Contains a start line (URI + Method/Status), headers & body

Headers describe the resource

Content-Type: text/html

<html>
  <title>Gabe Sullice</title> 
  <body>stout</body>
</html>
Content-Type: application/json

{
  "type": "user",
  "id": 1,
  "attributes": {
    "name": "gabesullice"
  }
}

Request

GET www.example.com/user/1 HTTP/1.1

response

HTTP/1.1 200 OK

 

Content-Type: application/json

{
  "type": "user",
  "id": 1,
  "attributes": {
    "name": "gabe"
  }
}

REST

Representational State transfer

not a protocol

REST is an architectural style that defines a set of constraints and properties based on HTTP.

Principles

  • Client-Server
  • Stateless
  • Cache
  • Layered System
  • Code-on-Demand
  • Uniform Interface

Client-server

A separation of concerns

Client handles presentation

Server handles data storage

Foundational principle for "decoupled" sites

STateless

Every request/response is independent

Cacheability

A response should contain information to govern its own intermediate storage

 

Its purpose is to eliminate requests/responses cycles or to reduce the time it takes for that cycle to complete (latency)

layered system

The system should be indifferent to proxies, load balancers, CDNs, etc.

COde-on-demand

Optional.

 

Useful when a client does not have the know-how on how to process a resource. It sends a request to a remote server for the code representing that know-how, receives that code, and executes it locally.

Uniform Interface

Entity representations are decoupled from their storage.


Hypermedia is the engine of application state.

HYPERmedia

 Hyper text

I'm a teapot, short and stout

✷Hyper✷text✷

<a href="https://httpstatus.es/218">I'm a teapot</a>

Hypermedia

{
  "type": "joke",
  "id": 1,
  "attributes": {
    "content": "I'm a teapot"
  },
  "links": {
    "explanation": "https://httpstatus.es/218"
  }
}

Hypermedia ➡ Web

index.html

index.html

hero.jpg

index.html

style.css

hero.jpg

index.html

style.css

url('https://www.example.com/assets/separator.svg');

hero.jpg

index.html

style.css

url('https://www.example.com/assets/hipster-heavy.ttf');

url('https://www.example.com/assets/separator.svg');

hero.jpg

/home

/posts

/posts/2

/posts/1

/about

/posts

/posts/2

/posts/2/tags

/posts/2/author

/posts/1

/posts

/posts/2

/posts/2/tags

/posts/2/author

/posts/1

/posts

/posts/2

/posts/2/tags

/posts/2/author

/posts/1

/posts/2

/posts/2/author

/posts/2/tags

/tags/2

/tags/1

index.html

style.css

/assets/list-bullet.svg

/assets/separator.svg

hero.jpg

HTTP/1.1

GET /index.html

GET /style.css

GET /assets/list-bullet.svg

GET /assets/separator.svg

GET /hero.jpg

HTTP/1.1

GET /index.html

GET /style.css

GET /assets/list-bullet.svg

GET /assets/separator.svg

GET /hero.jpg

HTTP/1.1

GET /index.html

GET /style.css

GET /assets/list-bullet.svg

GET /assets/separator.svg

GET /hero.jpg

HTTP/1.1

GET /index.html

GET /style.css

GET /assets/list-bullet.svg

GET /assets/separator.svg

GET /hero.jpg

HTTP/1.1

GET /index.html

GET /style.css

GET /assets/list-bullet.svg

GET /assets/separator.svg

GET /hero.jpg

HTTP/1.1

GET /index.html

GET /style.css

GET /assets/list-bullet.svg

GET /assets/separator.svg

GET /hero.jpg

HTTP/1.1

GET /index.html

GET /style.css

GET /assets/list-bullet.svg

GET /assets/separator.svg

GET /hero.jpg

HTTP/1.1

GET /index.html

GET /style.css

GET /assets/list-bullet.svg

GET /assets/separator.svg

GET /hero.jpg

HTTP/1.1 + BROWSER OPTIMIZATION

GET /index.html

GET /style.css

GET /assets/list-bullet.svg

GET /assets/separator.svg

GET /hero.jpg

HTTP/1.1 + BROWSER OPTIMIZATION

GET /index.html

GET /style.css

GET /assets/list-bullet.svg

GET /assets/separator.svg

GET /hero.jpg

HTTP/1.1 + BROWSER OPTIMIZATION

No OPTIMIZATION

optimized

time

HTTP/2

GET /index.html

GET /style.css

GET /assets/list-bullet.svg

GET /assets/separator.svg

GET /hero.jpg

HTTP/2

GET /index.html

GET /style.css

GET /assets/list-bullet.svg

GET /assets/separator.svg

GET /hero.jpg

HTTP/2 server push

time

Cooperation

Shared specifications

  • Contract between client and server
  • Impart additional meaning to:
    • Methods
      • POST == "create"
      • PATCH == "update"
    • Statuses
      • 200 means that it worked, no exceptions
    • Headers
      • Content-Type: application/vnd.api+json
    • Body
      • "The JSON will always look like this"

SCHEMA

  • "Entities of a type will always have representations that look like this"
  • "I will accept representations that look like this"

{
    "data": {
        "type": "person",
        "id": 1,
        "attributes": {
            "title": "Black Knight",
            "catchphrase": "'tis but a flesh wound.",
            "body": "dismembered"
        }
}

HATEOAS

  • Hypermedia As The Engine Of Application State
  • Are there more pages?
  • Can I delete this?
  • Does this cart have products?
  • Can I make a withdrawal?
  • What's acceptable for this resource?
{
    "data": [{
        "type": "post",
        "id": 1,
        "attributes": {
            "title": "modernism",
        },
        "relationships": {
            "comments": {
                "links": {
                    "related": "/api/posts/1/comments",
                    "create": "/api/posts/1/comments",
                    "schema": "/api/schemas/comments",
                }
            }
        },
        "links": {
            "delete": "/api/posts/1",
            "schema": "/api/schemas/post"
        }
    }],
    "links": {
        "self": "/api/content?page[offset]=1",
        "next": "/api/content?page[offset]=2",
        "schema": "/api/schemas/content",
        "create": {
            "href": "/api/content",
            "meta": {
                "acceptable-types": {
                    "post": "/api/schemas/post",
                    "note": "/api/schemas/note"
                }
            }
        }
    }
}
{
    "data": [{
        "type": "post",
        "id": 2,
        "attributes": {
            "title": "apocalypse",
        },
        "relationships": {
            "comments": {
                "links": {
                    "related": "/api/posts/2/comments",
                    "create": "/api/posts/2/comments",
                    "schema": "/api/schemas/comments",
                }
            }
        },
        "links": {
            "schema": "/api/schemas/post"
        }
    }],
    "links": {
        "self": "/api/content?page[offset]=2",
        "previous": "/api/content?page[offset]=1",
        "schema": "/api/schemas/content"
    }
}

/api/content

/api/schemas/content

/api/content?page=2

/api/posts/1/comments

/api/content

/api/schemas/content

/api/content?page=2

/api/content?page=3

/api/posts/2/comments

/api/posts/1/comments

/api/content

/api/schemas/content

/api/content?page=2

/api/content?page=3

/api/content?page=4

/api/posts/2/comments

/api/posts/1/comments

/api/posts/3/comments

GET /api/content

GET /api/content

GET /api/schemas/content

GET /api/content?page=1

GET /api/posts/1/comments

GET /api/content

GET /api/schemas/content

GET /api/content?page=1

GET /api/posts/1/comments

GET /api/posts/2/comments

GET /api/content?page=2

GET /api/content

GET /api/schemas/content

GET /api/content?page=1

GET /api/posts/1/comments

GET /api/posts/2/comments

GET /api/content?page=2

GET /api/posts/3/comments

. . .

GET /api/content?page=3

time

not so fast

  • If I'm the server
    • How do I know the client needs comments?
    • How do I know if the client needs the schema?
    • How do I know how many pages the client needs?

cooperate

by

communicating

Wha'dya'KNow?

  • If I'm the client:
    • I know I need 100 posts at most, but I don't know how many there are actually are.
    • I know if I have a schema, but I don't know if it's up-to-date.
    • I know I need comments, but I don't know if they exist.

 

  • If I'm the server:
    • I know how many posts I have, but I don't know how many the client needs.
    • I know the most up-to-date schema, but I don't know if the client needs it.
    • I know if there are comments or not.

We need a standard

HTTP + Hypermedia as the engine of application state

GET /api

X-Push-Please: [{
    "path": "{$.links.content.href}",
    "push-please": [{
        "path": "{$.links.next}",
        "quota": {
            "count": 100,
            "counter": {$.data}
        },
        "push-please": [{
            "path": "{$.data[*].relationships.comments.links.related}"
        }]
    }]
}, {
    "path": "{$.links.content.schema}/content"
}]

X-Cache-Digest: abc123def456ghi789cba321fed654ihg987...jkl1ef
GET /api HTTP/2

X-Push-Please: [{
    "path": "{$.links.content.href}",
    "push-please": [{
        "path": "{$.links.next}",
        "quota": {
            "count": 100,
            "counter": {$.data}
        },
        "push-please": [{
            "path": "{$.data[*].relationships.comments.links.related}"
        }]
    }]
}, {
    "path": "{$.links.content.meta.schema}"
}]
HTTP/2 200 OK

{
    "links": {
        "content": {
            "href": "/api/content",
            "meta": {
                "schema": "/api/schemas/content"
            }
        }
    }
}
Server gets initial request, generates response:

{
    "links": {
        "content": {
            "href": "/api/content",
            "meta": {
                "schema": "/api/schemas/content"
            }
        }
    }
}

Evaluates top-level push-please:

"{$.links.content.href}"                            => PUSH_PROMISE /api/content
"{$.links.content.meta.schema}"                     => PUSH_PROMISE /api/schemas/content

Move to /api/content scope, evaluate second-level push-please:

counts {$.data}                                     => counted = 33; count = 100 - counted;
if count > 0; "{$.links.next}"                      => PUSH_PROMISE /api/content?page=1
"{$.data[*].relationships.comments.links.related}"  => PUSH_PROMISE /api/post/1/comments, etc.

Move to /api/content?page=1 scope:

counts "{$.data}"                                   => counted = 47; count = 77 - counted;
if count > 0; "{$.links.next}"                      => PUSH_PROMISE /api/content?page=2
"{$.data[*].relationships.comments.links.related}"  => PUSH_PROMISE /api/post/2/comments, etc.

Move to /api/content?page=2 scope:

counts "{$.data}"                                   => counted = 30; count = 77 - counted;
if count > 0; "{$.links.next}"                      => DONE! count === 0



DEMO Time

Made with Slides.com