Kurt Griffiths | @kgriffs |
Everett Toews | @etoews |
Use the arrow keys to navigate
☺ +𝓡 =☁
[URL|RPC|CRUD] over HTTP
An architectural style for network-based applications
A set of constraints formalized by Roy Fielding
A counterpoint to RPC
“The shallow consider liberty a release from all law, from every constraint. The wise man sees in it, on the contrary, the potent Law of Laws.”
- Walt Whitman
Image credit: Nayu Kim
Image credit: dankreider
Image credit: Ωméga
Metadata about each message
Purpose and meaning (HTTP's GET/POST, Content-Type)
Override default behavior (HTTP's If-Modified-Since)
Image credit: Jan Kronquist
Image Credit: Kin Lane
Hint: Path Templating
swagger: "2.0"
info:
version: 1.0.0
title: A RESTful Adventure
host: localhost
schemes:
- http
consumes:
- application/json
produces:
- application/json
paths:
/characters:
???
Hint: Example Object and Resource Data Types
swagger: "2.0"
info:
version: 1.0.0
title: A RESTful Adventure
host: localhost
schemes:
- http
consumes:
- application/json
produces:
- application/json
paths:
/characters:
get:
responses:
200:
examples:
replace-this-with-a-mime-type: |-
{
"?": ?
...
}
post:
x-examples:
replace-this-with-a-mime-type: { "?": "?" }
responses:
201:
headers:
Location:
type: ?
format: ?
description: ?
examples:
replace-this-with-a-mime-type: |-
{
"?": "?"
...
}
Hint: Resource Data Types
swagger: "2.0"
info:
version: 1.0.0
title: A RESTful Adventure
host: localhost
schemes:
- http
consumes:
- application/json
produces:
- application/json
paths:
/characters:
...
definitions:
Character:
type: object
properties:
replace-this-with-name-of-property-1:
type: ?
format: ?
replace-this-with-name-of-property-2:
type: ?
format: ?
Characters:
type: array
items:
$ref: "#/definitions/Character"
Bonus Points: Use httpie and jq instead
HOST=https://104.130.13.137
echo "Get characters"
curl ? | python -m json.tool
echo "Create a character"
curl ?
{
"transaction_id": "71607e7c-df7c-45f3-b571-d1829de4ad9a",
"code": "736.9",
"title": "Teleport Denied",
"description": "The room you tried to visit does not exist or is not accessible from your current room. Thought you could get away with it didn't you.",
"link": {
"rel": "help",
"href": "https://en.wikipedia.org/wiki/No-teleportation_theorem"
}
}
Python Web Server Gateway Interface
def application(env, start_response):
body = 'Hello ' + env['HTTP_X_NAME'] + '!\n'
start_response("200 OK", [('Content-Type', 'text/plain')])
return [body.encode('utf-8')]
if __name__ == '__main__':
from wsgiref.simple_server import make_server
host = '127.0.0.1'
port = 8000
server = make_server(host, port, application)
print('Listening on {0}:{1}'.format(host, port))
server.serve_forever()
$ python myapp.py
Listening on 127.0.0.1:8000
$ pip install falcon
$ pip install httpie
$ mkdir restful-adventure && cd restful-adventure
$ wget https://raw.githubusercontent.com/etoews/a-restful-adventure/gh-pages/code/api.py -O api.py
$ wget https://raw.githubusercontent.com/etoews/a-restful-adventure/gh-pages/code/dal.py -O dal.py
class HelloResource(object):
def on_get(self, req, resp):
resp.body = 'Hello ' + req.get_header('x-name') + '!\n'
# Falcon defaults to 'application/json'
resp.content_type = 'text/plain'
# Falcon defaults to 200 OK
# resp.status = falcon.HTTP_200
# An instance of falcon.API is a WSGI application
api = falcon.API()
api.add_route('/', HelloResource())
$ python api.py
Listening on 127.0.0.1:8000
$ http 127.0.0.1:8000 x-name:Adventurer
HTTP/1.0 200 OK
Content-Length: 11
Content-Type: text/plain
Date: Wed, 18 Feb 2015 19:29:29 GMT
Server: WSGIServer/0.1 Python/2.7.9
Hello Adventurer!
Image Credit: PixelBlock
/characters:
get:
summary: List all Characters
operationId: list_characters
responses:
200:
description: An array of Characters
schema:
$ref: "#/definitions/Characters"
examples:
application/json: |-
{
"characters": [
{
"name": "Knox Thunderbane",
"links": [
{
"rel": "self",
"allow": [
"GET", "PUT"
],
"href": "/characters/1234"
},
{
"rel": "location",
"allow": [
"GET", "PUT"
],
"href": "/characters/1234/location"
}
]
}
],
"links": [
{
"rel": "self",
"allow": [
"GET", "POST"
],
"href": "/characters"
}
]
}
# api.add_route('/', HelloResource())
api.add_route('/characters', CharacterList(controller))
def on_get(self, req, resp):
# Ask the DAL for a list of entities
# TODO: If an error is raised, convert it to an instance
# of falcon.HTTPError
characters = self._controller.list_characters()
# Map the entities to the resource
resource = {
'characters': [self._entity_to_resource(c) for c in characters],
'links': [
{
'rel': 'self',
'allow': ['GET', 'POST'],
'href': '/characters'
}
]
}
# Create a JSON representation of the resource
resp.body = json.dumps(resource, ensure_ascii=False)
# Falcon defaults to the JSON media type for the content
# resp.content_type = 'application/json'
# Falcon defaults to 200 OK
# resp.status = falcon.HTTP_200
def _entity_to_resource(self, character):
base_href = self._id_to_href(character['id'])
links = [
{
'rel': 'self',
'allow': [
'GET', 'PUT'
],
'href': base_href
},
{
'rel': 'location',
'allow': [
'GET', 'PUT'
],
'href': base_href + '/location'
}
]
return {
'name': character['name'],
'links': links
}
def _id_to_href(self, character_id):
return '/characters/{0}'.format(character_id)
$ http 127.0.0.1:8000/characters
HTTP/1.0 200 OK
Date: Sat, 21 Feb 2015 22:52:54 GMT
Server: WSGIServer/0.1 Python/2.7.9
content-length: 258
content-type: application/json; charset=utf-8
{
"characters": [
{
"links": [
{
"allow": [
"GET",
"PUT"
],
"href": "/characters/c1a008bc-105f-4793-bfa6-a54fbc9ce6b1",
"rel": "self"
},
{
"allow": [
"GET",
"PUT"
],
"href": "/characters/c1a008bc-105f-4793-bfa6-a54fbc9ce6b1/location",
"rel": "location"
}
],
"name": "Knox Thunderbane"
}
],
"links": [
{
"allow": [
"GET",
"POST"
],
"href": "/characters",
"rel": "self"
}
]
}
Image Credit: Antirest
post:
summary: Create a Character
operationId: create_character
parameters:
- name: body
in: body
required: true
schema:
properties:
name:
type: string
minLength: 1
maxLength: 256
x-examples:
application/json: { "name": "Knox Thunderbane" }
responses:
201:
description: Character created
headers:
Location:
type: string
format: url
description: A link to the Character
schema:
$ref: "#/definitions/Character"
examples:
application/json: |-
{
"name": "Knox Thunderbane",
"links": [
{
"rel": "self",
"allow": [
"GET", "PUT"
],
"href": "/characters/1234"
},
{
"rel": "location",
"allow": [
"GET", "PUT"
],
"href": "/characters/1234/location"
}
]
}
default:
description: Unexpected errors
schema:
$ref: "#/definitions/Error"
def on_post(self, req, resp):
# Parse the incoming representation. This can be factored out into
# Falcon hooks or middleware, but we'll keep it inline for now.
# TODO: Validate against a schema
representation = req.stream.read().decode('utf-8')
representation = json.loads(representation)
# Create a new entity from the representation
# TODO: If an error is raised, convert it to an instance
# of falcon.HTTPError
character = self._controller.add_character(representation['name'])
# Map the entity to the resource. Again, this sort of thing
# could be factored out into a Falcon hook (DRY).
resource = self._entity_to_resource(character)
resp.location = self._id_to_href(character['id'])
resp.body = json.dumps(resource, ensure_ascii=False)
$ http OPTIONS 127.0.0.1:8000/characters
HTTP/1.0 204 No Content
Content-Length: 0
Date: Sat, 21 Feb 2015 22:59:18 GMT
Server: WSGIServer/0.1 Python/2.7.9
allow: GET, POST
$ http POST 127.0.0.1:8000/characters name="Commander Keen"
HTTP/1.0 200 OK
Date: Sat, 21 Feb 2015 23:00:02 GMT
Server: WSGIServer/0.1 Python/2.7.9
content-length: 254
content-type: application/json; charset=utf-8
location: /characters/8713e99b-d4d2-4855-85a2-9d0a32fec0b7
{
"links": [
{
"allow": [
"GET",
"PUT"
],
"href": "/characters/8713e99b-d4d2-4855-85a2-9d0a32fec0b7",
"rel": "self"
},
{
"allow": [
"GET",
"PUT"
],
"href": "/characters/8713e99b-d4d2-4855-85a2-9d0a32fec0b7/location",
"rel": "location"
}
],
"name": "Commander Keen"
}
$ http 127.0.0.1:8000/characters
HTTP/1.0 200 OK
Date: Sat, 21 Feb 2015 23:00:58 GMT
Server: WSGIServer/0.1 Python/2.7.9
content-length: 514
content-type: application/json; charset=utf-8
{
"characters": [
{
"links": [
{
"allow": [
"GET",
"PUT"
],
"href": "/characters/c1a008bc-105f-4793-bfa6-a54fbc9ce6b1",
"rel": "self"
},
{
"allow": [
"GET",
"PUT"
],
"href": "/characters/c1a008bc-105f-4793-bfa6-a54fbc9ce6b1/location",
"rel": "location"
}
],
"name": "Knox Thunderbane"
},
{
"links": [
{
"allow": [
"GET",
"PUT"
],
"href": "/characters/13742b78-be28-4662-b379-c9bba55b476c",
"rel": "self"
},
{
"allow": [
"GET",
"PUT"
],
"href": "/characters/13742b78-be28-4662-b379-c9bba55b476c/location",
"rel": "location"
}
],
"name": "Commander Keen"
}
],
"links": [
{
"allow": [
"GET",
"POST"
],
"href": "/characters",
"rel": "self"
}
]
}
Image Credit: Chester Bolingbroke
/dungeons:
get:
summary: List all Dungeons
operationId: list_dungeons
responses:
200:
description: An array of Dungeons
schema:
$ref: "#/definitions/Dungeons"
examples:
application/json: |-
{
"dungeons": [
{
"name": "Dungeon of Doom",
"links": [
{
"rel": "self",
"allow": [
"GET"
],
"href": "/dungeons/1234"
},
{
"rel": "room first",
"allow": [
"GET"
],
"href": "/dungeons/1234/rooms/1002",
"description": "entrance"
}
]
}
],
"links": [
{
"rel": "self",
"allow": [
"GET"
],
"href": "/dungeons"
}
]
}
def on_get(self, req, resp):
# Ask the DAL for a list of entities
dungeons = self._controller.list_dungeons()
# Map the entities to the resource
resource = {
'dungeons': [self._entity_to_resource(d) for d in dungeons],
'links': [
{
'rel': 'self',
'allow': ['GET'],
'href': '/dungeons'
}
]
}
# Create a JSON representation of the resource
resp.body = json.dumps(resource, ensure_ascii=False)
def _entity_to_resource(self, dungeon):
base_href = '/dungeons/{0}'.format(dungeon['id'])
links = [
{
'rel': 'self',
'allow': ['GET'],
'href': base_href
},
{
'rel': 'room first',
'allow': ['GET'],
'href': '{0}/rooms/{1}'.format(base_href, dungeon['entry_id'])
}
]
return {
'name': dungeon['name'],
'links': links
}
api.add_route('/dungeons', DungeonList(controller))
$ http 127.0.0.1:8000/dungeons
HTTP/1.0 200 OK
Date: Sun, 22 Feb 2015 02:13:32 GMT
Server: WSGIServer/0.1 Python/2.7.9
content-length: 564
content-type: application/json; charset=utf-8
{
"dungeons": [
{
"links": [
{
"allow": [
"GET"
],
"href": "/dungeons/5a024cd8-2db3-446e-b777-bdc60185a117",
"rel": "self"
},
{
"allow": [
"GET"
],
"href": "/dungeons/5a024cd8-2db3-446e-b777-bdc60185a117/rooms/8f726efc-5e3e-4332-ab24-243a1d3e0b27",
"rel": "room first"
}
],
"name": "Dungeon of Doom"
}
],
"links": [
{
"allow": [
"GET"
],
"href": "/dungeons",
"rel": "self"
}
]
}
$ http 127.0.0.1:8000/characters
HTTP/1.0 200 OK
Date: Sun, 22 Feb 2015 02:32:23 GMT
Server: WSGIServer/0.1 Python/2.7.9
content-length: 258
content-type: application/json; charset=utf-8
{
"characters": [
{
"links": [
{
"allow": [
"GET",
"PUT"
],
"href": "/characters/c1a008bc-105f-4793-bfa6-a54fbc9ce6b1",
"rel": "self"
},
{
"allow": [
"GET",
"PUT"
],
"href": "/characters/c1a008bc-105f-4793-bfa6-a54fbc9ce6b1/location",
"rel": "location"
}
],
"name": "Knox Thunderbane"
}
],
"links": [
{
"allow": [
"GET",
"POST"
],
"href": "/characters",
"rel": "self"
}
]
}
$ CHAR_LOCATION_URL=/characters/c1a008bc-105f-4793-bfa6-a54fbc9ce6b1/location
$ ROOM_URL=/dungeons/5a024cd8-2db3-446e-b777-bdc60185a117/rooms/8f726efc-5e3e-4332-ab24-243a1d3e0b27
$ http PUT 127.0.0.1:8000$CHAR_LOCATION_URL rel=room href=$ROOM_URL
HTTP/1.0 404 Not Found
Date: Fri, 20 Feb 2015 18:45:24 GMT
Server: WSGIServer/0.1 Python/2.7.9
content-length: 0
def on_put(self, req, resp, character_id):
# TODO: Validate against a schema
representation = req.stream.read().decode('utf-8')
representation = json.loads(representation)
# TODO: Raise falcon.HTTPError if ID is not a UUID
character_id = uuid.UUID(character_id)
# TODO: Raise falcon.HTTPError if ID is not a UUID
room_href = representation['href']
room_id = self._room_href_to_id(room_href)
# TODO: If an error is raised, convert it to an instance
# of falcon.HTTPError
self._controller.move_character(character_id, room_id)
# Success!
resp.status = falcon.HTTP_204
def on_get(self, req, resp, character_id):
# TODO: Handle the case that character_id is not a valid UUID
character_id = uuid.UUID(character_id)
# TODO: If an error is raised, convert it to an instance
# of falcon.HTTPError
room_id, dungeon_id = self._controller.get_location(character_id)
# Define the resource. We have to translate the DAL's notion
# of a "location" to the API's concept of a "location".
# TODO: If an error is raised, convert it to an instance
# of falcon.HTTPError
resource = self._room_id_to_location(room_id, dungeon_id)
# Create a JSON representation of the resource
# TODO: Use functools.partial to create a version of json.dumps that
# defaults to ensure_ascii=False
resp.body = json.dumps(resource, ensure_ascii=False)
def _room_href_to_id(self, href):
# ID will be the last part of the URL.
return uuid.UUID(href.split('/')[-1])
def _room_id_to_location(self, room_id, dungeon_id):
return {
'rel': 'room',
'allow': ['GET'],
'href': 'dungeons/{0}/rooms/{1}'.format(dungeon_id, room_id)
}
api.add_route('/characters/{character_id}/location', CharacterLocation(controller))
$ http PUT 127.0.0.1:8000$CHAR_LOCATION_URL rel=room href=$ROOM_URL
HTTP/1.0 204 No Content
Content-Length: 0
Date: Sun, 22 Feb 2015 03:35:06 GMT
Server: WSGIServer/0.1 Python/2.7.9
$ http 127.0.0.1:8000$CHAR_LOCATION_URL
HTTP/1.0 200 OK
Date: Sun, 22 Feb 2015 03:36:20 GMT
Server: WSGIServer/0.1 Python/2.7.9
content-length: 133
content-type: application/json; charset=utf-8
{
"allow": [
"GET"
],
"href": "dungeons/5a024cd8-2db3-446e-b777-bdc60185a117/rooms/8f726efc-5e3e-4332-ab24-243a1d3e0b27",
"rel": "room"
}
/dungeons/{dungeon_id}/rooms/{room_id}:
get:
summary: Get a specific Room in a specific Dungeon
operationId: get_room
parameters:
- name: dungeon_id
in: path
description: The id of the Dungeon
required: true
type: string
- name: room_id
in: path
description: The id of the Room
required: true
type: string
responses:
200:
description: Expected response to a valid request
schema:
$ref: "#/definitions/Room"
examples:
application/json: |-
{
"name": "Entrance",
"is_exit": false,
"links": [
{
"rel": "self",
"href": "/dungeons/1234/rooms/1000"
},
{
"rel": "room east",
"allow": [
"GET"
],
"href": "/dungeons/1234/rooms/1001"
}
]
}
400:
description: You tried to teleport. That's just not allowed.
schema:
$ref: "#/definitions/Error"
examples:
application/json: |-
{
"transaction_id": "71607e7c-df7c-45f3-b571-d1829de4ad9a",
"code": "736.9",
"title": "Teleport Denied",
"description": "The room you tried to visit does not exist or is not accessible from your current room. Thought you could get away with it didn't you.",
"link": {
"rel": "help",
"href": "https://en.wikipedia.org/wiki/No-teleportation_theorem"
}
}
default:
description: Unexpected errors
schema:
$ref: "#/definitions/Error"
def on_get(self, req, resp, dungeon_id, room_id):
# TODO: Handle the case that these are not valid UUIDs
dungeon_id = uuid.UUID(dungeon_id)
room_id = uuid.UUID(room_id)
# Note that we don't actually need the dungeon_id, since
# the DAL just wants the room_id. We'll just ignore it
# for now.
# TODO: If an error is raised, convert it to an instance
# of falcon.HTTPError
room = self._controller.get_room(room_id)
# Create a resource based on the room entity
# TODO: If an error is raised, convert it to an instance
# of falcon.HTTPError
resource = self._entity_to_resource(room)
# Create a JSON representation of the resource
resp.body = json.dumps(resource, ensure_ascii=False)
def _entity_to_resource(self, room):
dungeon_id = room['dungeon_id']
room_id = room['id']
base_href = 'dungeons/{0}'.format(dungeon_id)
links = [
{
'rel': 'self',
'allow': ['GET'],
'href': '{0}/rooms/{1}'.format(base_href, room_id)
},
{
'rel': 'dungeon',
'allow': ['GET'],
'href': base_href
}
]
# Add additional links, one per doorway to another room
links.extend([
{
'rel': 'room ' + doorway['direction'],
'allow': ['GET'],
'href': self._id_to_href(doorway['room_id'], dungeon_id)
}
for doorway in room['doorways']
])
return {
'name': room['name'],
'is_exit': room['is_exit'],
'links': links
}
def _id_to_href(self, room_id, dungeon_id):
return '/dungeons/{0}/rooms/{1}'.format(dungeon_id, room_id)
api.add_route('/dungeons/{dungeon_id}/rooms/{room_id}', Room(controller))
$ http 127.0.0.1:8000/dungeons
HTTP/1.0 200 OK
Date: Tue, 24 Feb 2015 00:44:24 GMT
Server: WSGIServer/0.2 CPython/3.3.6
content-length: 356
content-type: application/json; charset=utf-8
{
"dungeons": [
{
"links": [
{
"allow": [
"GET"
],
"href": "/dungeons/5a024cd8-2db3-446e-b777-bdc60185a117",
"rel": "self"
},
{
"allow": [
"GET"
],
"href": "/dungeons/5a024cd8-2db3-446e-b777-bdc60185a117/rooms/8f726efc-5e3e-4332-ab24-243a1d3e0b27",
"rel": "room first"
}
],
"name": "Dungeon of Doom"
}
],
"links": [
{
"allow": [
"GET"
],
"href": "/dungeons",
"rel": "self"
}
]
}
$ ROOM_URL=/dungeons/5a024cd8-2db3-446e-b777-bdc60185a117/rooms/8f726efc-5e3e-4332-ab24-243a1d3e0b27
$ http PUT 127.0.0.1:8000$CHAR_LOCATION_URL rel=room href=$ROOM_URL
HTTP/1.0 204 No Content
Content-Length: 0
Date: Sun, 22 Feb 2015 03:56:54 GMT
Server: WSGIServer/0.1 Python/2.7.9
$ http 127.0.0.1:8000$ROOM_URL
HTTP/1.0 200 OK
Date: Tue, 24 Feb 2015 01:03:13 GMT
Server: WSGIServer/0.2 CPython/3.3.6
content-length: 575
content-type: application/json; charset=utf-8
{
"is_exit": false,
"links": [
{
"allow": [
"GET"
],
"href": "dungeons/5a024cd8-2db3-446e-b777-bdc60185a117/rooms/8f726efc-5e3e-4332-ab24-243a1d3e0b27",
"rel": "self"
},
{
"allow": [
"GET"
],
"href": "dungeons/5a024cd8-2db3-446e-b777-bdc60185a117",
"rel": "dungeon"
},
{
"allow": [
"GET"
],
"href": "/dungeons/5a024cd8-2db3-446e-b777-bdc60185a117/rooms/751d5812-144d-40f8-a82d-221dbb3075e2",
"rel": "room north"
},
{
"allow": [
"GET"
],
"href": "/dungeons/5a024cd8-2db3-446e-b777-bdc60185a117/rooms/65840050-12d3-4e29-9412-7c3b22fdd52e",
"rel": "room east"
}
],
"name": "Super Creepy Entrance"
}
Thou findest thyself in yonder Super Creepy Entrance...
Looking about, thou doth see doorways leading North and East. What
is thy command?
> _
Thou findest thyself in yonder Super Creepy Entrance...
Looking about, thou doth see doorways leading North and East. What
is thy command?
> go north_
$ ROOM_URL=/dungeons/5a024cd8-2db3-446e-b777-bdc60185a117/rooms/751d5812-144d-40f8-a82d-221dbb3075e2
$ http PUT 127.0.0.1:8000$CHAR_LOCATION_URL rel=room href=$ROOM_URL
HTTP/1.0 204 No Content
Content-Length: 0
Date: Sun, 22 Feb 2015 03:56:54 GMT
Server: WSGIServer/0.1 Python/2.7.9
$ http 127.0.0.1:8000$ROOM_URL
HTTP/1.0 200 OK
Date: Sun, 22 Feb 2015 03:58:01 GMT
Server: WSGIServer/0.1 Python/2.7.9
content-length: 417
content-type: application/json; charset=utf-8
{
"is_exit": false,
"links": [
{
"allow": [
"GET"
],
"href": "dungeons/5a024cd8-2db3-446e-b777-bdc60185a117/rooms/751d5812-144d-40f8-a82d-221dbb3075e2",
"rel": "self"
},
{
"allow": [
"GET"
],
"href": "dungeons/5a024cd8-2db3-446e-b777-bdc60185a117",
"rel": "dungeon"
},
{
"allow": [
"GET"
],
"href": "/dungeons/5a024cd8-2db3-446e-b777-bdc60185a117/rooms/8f726efc-5e3e-4332-ab24-243a1d3e0b27",
"rel": "room south"
}
],
"name": "Armory"
}
Thou findest thyself in yonder Armory...
Fortunately, thou art not dead yet. Be that at is may, thou hast
found a dead-end. Thou canst only retreat back through yonder
Southward door. What is thy command?
> _
$ http 127.0.0.1:8000/characters
HTTP/1.0 200 OK
Date: Sun, 22 Feb 2015 04:21:58 GMT
Server: WSGIServer/0.1 Python/2.7.9
content-length: 258
content-type: application/json; charset=utf-8
{
"characters": [
{
"links": [
{
"allow": [
"GET",
"PUT"
],
"href": "/characters/c1a008bc-105f-4793-bfa6-a54fbc9ce6b1",
"rel": "self"
},
{
"allow": [
"GET",
"PUT"
],
"href": "/characters/c1a008bc-105f-4793-bfa6-a54fbc9ce6b1/foo",
"rel": "location"
}
],
"name": "Knox Thunderbane"
}
],
"links": [
{
"allow": [
"GET",
"POST"
],
"href": "/characters",
"rel": "self"
}
]
}
$ CHAR_LOCATION_URL=/characters/c1a008bc-105f-4793-bfa6-a54fbc9ce6b1/foo
$ http PUT 127.0.0.1:8000$CHAR_LOCATION_URL rel=room href=$ROOM_URL