Writing and consuming REST services

Chris St. Pierre - @chris_st_pierre

Follow along at stpierre.github.io/REST/

REpresentational
State
Transfer

But why?


import sandwiches

sandwiches.make_me_a_sandwich("ham")
          

curl -X POST -d '{"type": "ham"}' \
    https://sandwiches.example.com/sandwiches
          

Let's talk about good REST APIs.

Let's talk about practical REST APIs.

Let's be liberal in what we accept.

What about SOAP and XML-RPC?

Verbs (or methods) and
resources (or endpoints)


              GET http://example.com/v1/pets/ HTTP/1.0
            
{
  "pets": [
    {
      "id": "a8533344-6371-4982-a86b-722331839514",
      "links": [
        {
          "href": "/v1/pets/a8533344-6371-4982-a86b-722331839514",
          "rel": "self"
        }
      ]
    },
    {
      "id": "00182d56-9981-402b-821e-7b1c2906533c",
      "links": [
        {
          "href": "/v1/pets/00182d56-9981-402b-821e-7b1c2906533c",
          "rel": "self"
        }
      ]
    },
    {
      "id": "18e1588a-090e-4ace-94c2-f289617de0cb",
      "links": [...]
    },
    {
      "id": "4dc4d7b0-a386-4919-9878-47d4eb8f49ec",
      "links": [...]
    },
    {
      "id": "bf61616c-010e-48f2-8173-78b330804cd6",
      "links": [...]
    },
    {
      "id": "d25dcdd0-28f4-4fd3-9e5f-da1d9d224940",
      "links": [...]
    },
    {
      "id": "72f12e94-33ca-4537-9897-205ffe42350e",
      "links": [...]
    }
  ]
}
          

GET

List/read


              $ curl http://example.com/v1/owners/
            
{
  "owners": [
    {
      "id": "511363b2-8693-4b30-ae4c-09964a4cebe0",
      "links": [
        {
          "href": "/v1/owners/511363b2-8693-4b30-ae4c-09964a4cebe0",
          "rel": "self"
        }
      ]
    },
    {
      "id": "ee5479d6-7070-4109-bfb2-d44e3c42782b",
      "links": [...]
    },
    {
      "id": "35a85559-dc03-4aa2-85d2-947e17e310e5",
      "links": [...]
    },
    {
      "id": "5e174bc4-4135-4e7b-b957-9705bacc903d",
      "links": [...]
    }
  ]
}
          

$ curl http://example.com/v1/owners/511363b2-8693-4b30-ae4c-09964a4cebe0
            
{
  "id": "511363b2-8693-4b30-ae4c-09964a4cebe0",
  "name": "Goran Shain",
  "birthday": "1981-10-27",
  "shoe_size": 10.5,
  "pets": [
    "a8533344-6371-4982-a86b-722331839514",
    "d25dcdd0-28f4-4fd3-9e5f-da1d9d224940"
  ],
  "links": [
    {
      "href": "/v1/owners/511363b2-8693-4b30-ae4c-09964a4cebe0",
      "rel": "self"
    },
    {
      "href": "/v1/pets/a8533344-6371-4982-a86b-722331839514",
      "rel": "pet"
    },
    {
      "href": "/v1/pets/d25dcdd0-28f4-4fd3-9e5f-da1d9d224940",
      "rel": "pet"
    }
  ]
}
          

              $ curl http://example.com/v1/owners/\?limit=1\&detail=1
            
{
  "owners": [
    {
      "id": "511363b2-8693-4b30-ae4c-09964a4cebe0",
      "name": "Goran Shain",
      "birthday": "1981-10-27",
      "shoe_size": 10.5,
      "pets": [
        "a8533344-6371-4982-a86b-722331839514",
        "d25dcdd0-28f4-4fd3-9e5f-da1d9d224940"
      ],
      "links": [
        {
          "href": "/v1/owners/511363b2-8693-4b30-ae4c-09964a4cebe0",
          "rel": "self"
        },
        {
          "href": "/v1/pets/a8533344-6371-4982-a86b-722331839514",
          "rel": "pet"
        },
        {
          "href": "/v1/pets/d25dcdd0-28f4-4fd3-9e5f-da1d9d224940",
          "rel": "pet"
        }
      ]
    }
  ]
}
          
/v1/owners
/v1/owners/511363b2-8693-4b30-ae4c-09964a4cebe0
/v1/owners/511363b2-8693-4b30-ae4c-09964a4cebe0/pets/?limit=5
/v1/servers/b8060d42-8a2c-4f38-8ffb-b1b749e7a50a/reboot

$ curl http://example.com/v1/owners/ee5479d6-7070-4109-bfb2-d44e3c42782b
            
{
  "id": "ee5479d6-7070-4109-bfb2-d44e3c42782b",
  "name": "Remus Bergfalk",
  "birthday": "1977-05-08",
  "shoe_size": 9.5,
  "pets": [
    "00182d56-9981-402b-821e-7b1c2906533c",
    "72f12e94-33ca-4537-9897-205ffe42350e"
  ],
  "links": [
    {
      "href": "/v1/owners/ee5479d6-7070-4109-bfb2-d44e3c42782b",
      "rel": "self"
    },
    {
      "href": "/v1/pets/00182d56-9981-402b-821e-7b1c2906533c",
      "rel": "pet"
    },
    {
      "href": "/v1/pets/72f12e94-33ca-4537-9897-205ffe42350e",
      "rel": "pet"
    }
  ]
}
            

Hypertext
As
The
Engine
Of
Application
State

HEAD

Check existence


$ curl -X HEAD \
   http://example.com/v1/owners/511363b2-8693-4b30-ae4c-09964a4cebe0
            
              # HTTP code: 200
          

$ curl -X HEAD \
   http://example.com/v1/owners/totally-bogus-owner-uuid
            
              # HTTP code: 404
          

POST

Create


$ curl -X POST -d '{"name":"Jovka Garver","birthday":"1983-02-26"}' \
    -H 'Content-Type: application/json' http://example.com/v1/owners/
            
{
  "id": "b90a363c-d694-4e8c-be68-f53eeece2b99",
  "name": "Jovka Garver",
  "birthday": "1983-02-26",
  "pets": []
}
            
              # HTTP code: 201
          

              $ curl http://example.com/v1/owners/
            
{
  "owners": [
    "511363b2-8693-4b30-ae4c-09964a4cebe0",
    "ee5479d6-7070-4109-bfb2-d44e3c42782b",
    "35a85559-dc03-4aa2-85d2-947e17e310e5",
    "5e174bc4-4135-4e7b-b957-9705bacc903d",
    "b90a363c-d694-4e8c-be68-f53eeece2b99"
  ]
}
          

$ curl http://someapp.example.com/v1/books/1
            
{"title": "Moby Dick", "text": "Call me Ishmael. Some years ago...."}
          

$ curl -H "Accept: text/plain" http://someapp.example.com/v1/books/1
            
Moby Dick

Call me Ishmael. Some years ago....
          

$ curl -H "Accept-Encoding: gzip" http://someapp.example.com/v1/books/1 | \
      gunzip -c
            
{"title": "Moby Dick", "text": "Call me Ishmael. Some years ago...."}
          

$ curl -X POST -d '' \
    http://example.com/v1/servers/335b53b4-7b86-4a3b-976e-75f13a0a9e78/reboot
            
              # HTTP code: 204
          

PUT

Create/replace (idempotent)


$ curl http://example.com/v1/owners/35a85559-dc03-4aa2-85d2-947e17e310e5
            
{
  "id": "35a85559-dc03-4aa2-85d2-947e17e310e5",
  "name": "Jolanda Seaver",
  "birthday": "1996-08-29",
  "shoe_size": 8.5,
  "pets": [
    "18e1588a-090e-4ace-94c2-f289617de0cb",
    "4dc4d7b0-a386-4919-9878-47d4eb8f49ec"
  ]
}
          

$ curl -X PUT \
    -d '{"name":"Jolanda Seaver","birthday":"1996-08-29","shoe_size":9}' \
    -H 'Content-Type: application/json' \
    http://example.com/v1/owners/35a85559-dc03-4aa2-85d2-947e17e310e5
            
{
  "id": "35a85559-dc03-4aa2-85d2-947e17e310e5",
  "name": "Jolanda Seaver",
  "birthday": "1996-08-29",
  "shoe_size": 9.0,
  "pets": [
    "18e1588a-090e-4ace-94c2-f289617de0cb",
    "4dc4d7b0-a386-4919-9878-47d4eb8f49ec"
  ]
}
          

$ curl -X PUT -d '{"shoe_size":9}' -H 'Content-Type: application/json' \
    http://example.com/v1/owners/35a85559-dc03-4aa2-85d2-947e17e310e5

{"message": {"name": "Missing required parameter in the JSON body or the post body or the query string"}}
            
              # HTTP code: 400
          

$ curl \
    http://example.com/v1/veterinarians/3cfbb699-d2f2-45cd-b932-be5171887262
            
{
  "id": "3cfbb699-d2f2-45cd-b932-be5171887262",
  "name": "Seong-Min Park",
  "patients": [
    "00182d56-9981-402b-821e-7b1c2906533c",
  ]
}
          

$ curl \
    http://example.com/v1/veterinarians/3cfbb699-d2f2-45cd-b932-be5171887262/patients/
            
{
  "patients": [
    "00182d56-9981-402b-821e-7b1c2906533c",
  ]
}
          

$ curl -X PUT \
    http://example.com/v1/veterinarians/3cfbb699-d2f2-45cd-b932-be5171887262/patients/d25dcdd0-28f4-4fd3-9e5f-da1d9d224940
            
{
  "patient": {
    "id": "d25dcdd0-28f4-4fd3-9e5f-da1d9d224940"
  },
  "veterinarian": {
    "id": "3cfbb699-d2f2-45cd-b932-be5171887262"
  }
}
            
              # HTTP code: 201
            

$ curl \
    http://example.com/v1/veterinarians/3cfbb699-d2f2-45cd-b932-be5171887262/patients/
            
{
  "patients": [
    "00182d56-9981-402b-821e-7b1c2906533c",
    "d25dcdd0-28f4-4fd3-9e5f-da1d9d224940"
  ]
}
          

$ curl -X PUT \
    http://example.com/v1/veterinarians/3cfbb699-d2f2-45cd-b932-be5171887262/patients/d25dcdd0-28f4-4fd3-9e5f-da1d9d224940
            
{
  "patient": {
    "id": "d25dcdd0-28f4-4fd3-9e5f-da1d9d224940"
  },
  "veterinarian": {
    "id": "3cfbb699-d2f2-45cd-b932-be5171887262"
  }
}
            
              # HTTP code: 200
            

$ curl -X POST -d '{"pet_id":"d25dcdd0-28f4-4fd3-9e5f-da1d9d224940"}' \
    -H 'Content-Type: application/json' \
    http://example.com/v1/veterinarians/3cfbb699-d2f2-45cd-b932-be5171887262/patients/
            
              {"message": "Pet d25dcdd0-28f4-4fd3-9e5f-da1d9d224940 is already assigned to veterinarian 3cfbb699-d2f2-45cd-b932-be5171887262"}
            
              # HTTP code: 409
            

Demonstrating Relationships

One-to-one, one-to-many, many-to-many


$ curl http://example.com/v1/pets/72f12e94-33ca-4537-9897-205ffe42350e
            
{
  "id": "72f12e94-33ca-4537-9897-205ffe42350e",
  "name": "Hellbringer the Befouler",
  "breed": "domestic shorthair",
  "veterinarian": "fd87a620-466d-41ae-a6b0-1527b5126c58",
  "species": "cat",
  "owners": [
    "ee5479d6-7070-4109-bfb2-d44e3c42782b"
  ]
}
          

$ curl \
    http://example.com/v1/pets/72f12e94-33ca-4537-9897-205ffe42350e/veterinarian/
            
{
  "veterinarian": "fd87a620-466d-41ae-a6b0-1527b5126c58"
}
          

$ curl \
    http://example.com/v1/pets/72f12e94-33ca-4537-9897-205ffe42350e/veterinarian/\?detail=1
            
{
  "veterinarian": {
    "id": "fd87a620-466d-41ae-a6b0-1527b5126c58",
    "name": "Colobert Bannerman",
    "specialty": "cats"
  }
}
          

$ curl http://example.com/v1/pets/4dc4d7b0-a386-4919-9878-47d4eb8f49ec
            
{
  "id": "4dc4d7b0-a386-4919-9878-47d4eb8f49ec",
  "name": "Scarface",
  "breed": "English Bulldog",
  "veterinarian": "f5df1cc0-4fa2-4605-af57-4da6479e8afa",
  "species": "dog",
  "owners": [
    "35a85559-dc03-4aa2-85d2-947e17e310e5",
    "5e174bc4-4135-4e7b-b957-9705bacc903d"
  ]
}
          

$ curl \
    http://example.com/v1/pets/4dc4d7b0-a386-4919-9878-47d4eb8f49ec/owners/
            
{
  "owners": [
    "35a85559-dc03-4aa2-85d2-947e17e310e5",
    "5e174bc4-4135-4e7b-b957-9705bacc903d"
  ]
}
          

$ curl http://example.com/v2/ownership/5f5c1ded-32e7-4fb2-9b6f-b0aa05616d67
            
{
  "id": "5f5c1ded-32e7-4fb2-9b6f-b0aa05616d67",
  "owner": "511363b2-8693-4b30-ae4c-09964a4cebe0",
  "pet": "a8533344-6371-4982-a86b-722331839514",
}
          

PATCH

Update


$ curl -X PATCH -d '{"shoe_size":9.5}' -H 'Content-Type: application/json' \
    http://example.com/v1/owners/35a85559-dc03-4aa2-85d2-947e17e310e5
            
{
  "id": "35a85559-dc03-4aa2-85d2-947e17e310e5",
  "name": "Jolanda Seaver",
  "birthday": "1996-08-29",
  "shoe_size": 9.5,
  "pets": [
    "18e1588a-090e-4ace-94c2-f289617de0cb",
    "4dc4d7b0-a386-4919-9878-47d4eb8f49ec"
  ]
}
          

$ curl http://example.com/v1/pets/bf61616c-010e-48f2-8173-78b330804cd6
            
{
  "id": "bf61616c-010e-48f2-8173-78b330804cd6",
  "name": "Kisses",
  "species": "Hissing Cockroach",
  "veterinarian": null,
  "owners": []
}
          

$ curl -X PATCH \
    -d '{"veterinarian":"3cfbb699-d2f2-45cd-b932-be5171887262","owners":["ee5479d6-7070-4109-bfb2-d44e3c42782b"]}' \
    -H 'Content-Type: application/json' \
    http://example.com/v1/pets/bf61616c-010e-48f2-8173-78b330804cd6
            
{
  "id": "bf61616c-010e-48f2-8173-78b330804cd6",
  "name": "Kisses",
  "species": "Hissing Cockroach",
  "veterinarian": "3cfbb699-d2f2-45cd-b932-be5171887262",
  "owners": [
    "ee5479d6-7070-4109-bfb2-d44e3c42782b"
  ]
}
            
              # HTTP code: 200
          

$ data='[
  {"node": "veterinarian",
   "op": "change",
   "value": "f5df1cc0-4fa2-4605-af57-4da6479e8afa"},
  {"node": "owner",
   "op": "delete",
   "value": "ee5479d6-7070-4109-bfb2-d44e3c42782b"}
]'
$ curl -X PATCH -d "$data" -H 'Content-Type: application/json' \
    http://example.com/v2/pets/bf61616c-010e-48f2-8173-78b330804cd6
            
{
  "id": "bf61616c-010e-48f2-8173-78b330804cd6",
  "name": "Kisses",
  "species": "Hissing Cockroach",
  "veterinarian": "f5df1cc0-4fa2-4605-af57-4da6479e8afa",
  "owners": []
}
          

DELETE


$ curl -X DELETE \
    http://example.com/v1/owners/35a85559-dc03-4aa2-85d2-947e17e310e5
            
              # HTTP code: 204
          

Return codes


$ curl -X DELETE \
    http://example.com/v1/owners/35a85559-dc03-4aa2-85d2-947e17e310e5
            
              {"message": "No such owner 35a85559-dc03-4aa2-85d2-947e17e310e5"}
            
              # HTTP code: 404
          

$ curl -X POST -d '{"pet_id":"a8533344-6371-4982-a86b-722331839514"}' \
    -H 'Content-Type: application/json' \
    http://example.com/v1/veterinarians/f5df1cc0-4fa2-4605-af57-4da6479e8afa/patients/
            
{"message": "Pet a8533344-6371-4982-a86b-722331839514 is already assigned to veterinarian f5df1cc0-4fa2-4605-af57-4da6479e8afa"}
            
              # HTTP code: 409
            
Verb Collection operation Record operation
GET List Read
HEAD N/A Check existence
POST Create N/A
PUT N/A Create/replace (idempotent)
PATCH N/A Partial update
DELETE Delete (rarely) Delete

Authentication


$ curl -X DELETE \
    http://example.com/v2/owners/5e174bc4-4135-4e7b-b957-9705bacc903d
            
{"headers": {"WWW-Authenticate": "Basic realm=\"Login Required\""}, "message": "Request requires authentication"}
            
              # HTTP code: 401
          

$ curl -X DELETE -u stpierre:hunter2 \
    http://example.com/v2/owners/5e174bc4-4135-4e7b-b957-9705bacc903d
            
              # HTTP code: 204
          

$ curl -u stpierre:hunter2 http://example.com/v2/token/
            
{
  "token": "6158a4ff-8d50-47bd-8316-c0008b7cbc88"
}
          

$ curl -X DELETE -H 'X-Token: 6158a4ff-8d50-47bd-8316-c0008b7cbc88' \
    http://example.com/v2/pets/18e1588a-090e-4ace-94c2-f289617de0cb
            
              # HTTP code: 204
          

Async


              $ curl http://example.com/v2/get-nth-prime/?n=2
            
              {"n": 2, "prime": 2}
          

              $ curl http://example.com/v2/get-nth-prime/?n=22389047890
          

              $ curl http://example.com/v2/get-nth-prime/?n=22389047890
            
{
  "task": {
    "id": "e8a09eb7-a83e-4d4d-8d31-1208bdb7f990",
    "status": "QUEUED"
  }
}
            
              # HTTP code: 202
          

$ curl http://example.com/v2/tasks/e8a09eb7-a83e-4d4d-8d31-1208bdb7f990
            
{
  "task": {
    "id": "e8a09eb7-a83e-4d4d-8d31-1208bdb7f990",
    "status": "PENDING"
  }
}
            
              # HTTP code: 200
          

$ curl http://example.com/v2/tasks/e8a09eb7-a83e-4d4d-8d31-1208bdb7f990
            
{
  "task": {
    "id": "e8a09eb7-a83e-4d4d-8d31-1208bdb7f990",
    "status": "COMPLETE",,
    "result": {"n": 22389047890, "prime": 583235838259}
  }
}
            
              # HTTP code: 200
          

$ curl http://example.com/v2/tasks/e8a09eb7-a83e-4d4d-8d31-1208bdb7f990
            
{
  "task": {
    "id": "e8a09eb7-a83e-4d4d-8d31-1208bdb7f990",
    "status": "ERROR",
    "error": {
      "message": "Ran out of numbers, contact your sysadmin to add more"
    }
  }
}
            
              # HTTP code: 200
          

$ curl -X POST -d '{"image": "myimage", "flavor": "m1.tiny"}' \
    http://example.com/v2/servers
            
{
  "instance": {
    "id": "3a7ab55d-83cd-4cc0-aca0-e8feab4fb62b",
    "hostname": "i-908af05e",
    "image": "myimage",
    ...
    "status": "CREATING"
  }
}
            
              # HTTP code: 202
          

?