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"
- Methods
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
Building "cooperative" APIs with HTTP/2
By Gabriel Sullice (Gabe)
Building "cooperative" APIs with HTTP/2
- 597