RESTful web services are the rage. APIs for public consumption are everywhere and developers are being tasked daily with the chore of implementing new ones, and ColdBox has a fantastic set of tools for helping you to do just that. As developers, though, we often fall in to the trap of thinking of API's only in terms of delivering data when, in reality, with a bit of foresight and architectural planning, we can create API's that enable conversations with our consumers. In a recent Coldbox Restful Roadshow presentation, two of the "Lessons Learned" I noted, from developing and working with ReSTful APIs over the past few years, included these two recommendations:
- Use all of the ( HTTP ) Verbs
- Use all of the ( HTTP Status ) Codes
By taking the time to learn the language of HTTP verbs and status codes, a developer can implement APIs that enable rich, interactive conversations with consumers of their service. Let's say I were to take a trip across the pond from the US to visit the UK. Upon arrival, I check in to my hotel and then make a trip to the pub down the street for a taste of the local brew and, maybe, some grub.
Now, let's re-envision my trip to the corner pub as if it were an ongoing conversation between an API consumer and provider. Again, it's just a corner pub, so that API is fairly limited:
- THIRSTY DEVELOPER: "Hello! Just flew in to town (and, boy, are my arms tired!). May I see a menu?
HTTP Request:
To start out, I send an OPTIONS request to the pub's representative to find out what's available:Request URL: https://cornerpub.co.uk/api Accept: application/json Request Method: OPTIONS
- CORNER CORNER PUB: "Here's a menu. Have at it, Yank!"
HTTP Response:
The bartender provides me with the pub's options which includes the menu:Status Code: 200 Content Type: application/json Response: { "menu": { "food": "/api/food" "drinks" "/api/drinks" } "order": { "POST":{ "description": "Create an order", "href": "/orders", "params": { "firstName":{ "required":true, "type":"string", "description":"Customer's first name" }, "lastName":{ "required":false, "type":"string", "description":"Customer's last name" }, "tableNumber":{ "required":false, "type":"integer", "description":"The customers table number. Required if not seated at the bar." }, "barSeating":{ "required":false, "default": false, "type":"boolean", "description":"Whether the patron is seated at the bar." } }, } } }
- THIRSTY DEVELOPER: "Great, I'm thirsty let's see what's on the drink menu."
HTTP Request:Request URL: https://cornerpub.co.uk/api/drinks Accept: application/json Request Method: OPTIONS
- Like turning the page to the drink menu, the API responds again with:
Status Code: 200 Content Type: application/json Response: { "beer": { "title": "Beer Menu", "params":{ "roomtemp":{ "type":"boolean", "required":false, "description": "Beer which is served at room temperature" }, "cold":{ "type":"boolean", "required":false, "description": "Beer which is served cold" }, } "href":"/api/drinks/beer" }, "wine": { "title": "Wine Menu", "params":{ "red":"Red wine", "white":"White wine" } "href":"/api/drinks/wine" }, "mixed": { "title": "Mixed Drinks", "params" {} "href":"/api/drinks/mixed" } }
- THIRSTY DEVELOPER: "A cold beer sounds great! Let's see what you have!"
HTTP Request:Request URL: https://cornerpub.co.uk/api/drinks/beer?cold=true Accept: application/json Request Method: GET
- CORNER PUB: "You're in the UK, Yank. We don't have any cold beer."
-
Status Code: 204 Content Type: application/json
- THIRSTY DEVELOPER: "OK, fine. I'm not used to room temperature beer but, since that's all you have, may I see that menu please?"
- Since there were only two options and one of them didn't have anything, I simply request to see the full list:
HTTP Request:Request URL: https://cornerpub.co.uk/api/drinks/beer Accept: application/json Request Method: GET
- CORNER PUB: "Sure, mate. Here you go - 5 at a time:"
- Our pub responds with a status of
206
which signifies partial content. - Note that rather just dumping an array of items, the pubs response separates the sorting of the products from a top-level hashmap. From a performance standpoint, this makes the consumption, re-sorting, and locating of nested data within the individual results much more efficient for the consumer, as well as for the pub, when it comes time to order.
- The API response also includes additional information on the recordset and paging:
Status Code: 206 Content Type: application/json Response: { "recordset":{ "total": 35, "limit": 5 "order" : "brand", "orderDirection": "asc", "sorted":[ "2743ce3ea2e04dbf972f8873a7f6e3c7", "f6c4198809ce433ea3dce954bd95719b", "5dd41ab29f584bc68e2563c67c5d271f", "111ca56ba1e14cd3bb707e30c4a816da", "82058b6576af41a0a04ea1b457423d32" ], "href": { "next":"/drinks/beer?page=2" } }, "beers":{ "82058b6576af41a0a04ea1b457423d32": { "name": "Bishop's Farewell", "href": "/drinks/beer/82058b6576af41a0a04ea1b457423d32", "order": { "href":"/order", "params":{ "menutype":"beer", "id":"82058b6576af41a0a04ea1b457423d32" } } }, "111ca56ba1e14cd3bb707e30c4a816da": { "name": "Guinness Foreign Extra Stout", "href": "/drinks/beer/111ca56ba1e14cd3bb707e30c4a816da", "order": { "href":"/order", "params":{ "menutype":"beer", "id":"111ca56ba1e14cd3bb707e30c4a816da" } } }, "f6c4198809ce433ea3dce954bd95719b": { "name": "Imperial Brown Stout London 1856", "href": "/drinks/beer/f6c4198809ce433ea3dce954bd95719b", "order": { "href":"/order", "params":{ "menutype":"beer", "id":"f6c4198809ce433ea3dce954bd95719b" } } }, "2743ce3ea2e04dbf972f8873a7f6e3c7": { "name": "Kipling", "href": "/drinks/beer/2743ce3ea2e04dbf972f8873a7f6e3c7", "order": { "href":"/order", "params":{ "menutype":"beer", "id":"2743ce3ea2e04dbf972f8873a7f6e3c7" } } }, "5dd41ab29f584bc68e2563c67c5d271f": { "name": "Landlord Pale Ale", "href": "/drinks/beer/5dd41ab29f584bc68e2563c67c5d271f", "order": { "href":"/order", "params":{ "menutype":"beer", "id":"5dd41ab29f584bc68e2563c67c5d271f" } } } } }
- THIRSTY DEVELOPER: "Great! I'd like to place an order."
HTTP Request:Request URL: https://cornerpub.co.uk/api/orders Accept: application/json Request Method: POST Request Body: { "firstName":"Jon", "barSeating":true }
- CORNER PUB: "Very well. Here's your order information:"
-
The pub responds with a status of
201
, signifying that the order has been created and provides us with the endpoint for future operations:
Status Code: 201 Content Type: application/json Response: { "id":"fb5331bf7d9e4dae90f25a190b16037f", "href":"/api/orders/fb5331bf7d9e4dae90f25a190b16037f", }
THIRSTY DEVELOPER: "Perfect. I'd like to order a beer. Tell me how."
- HTTP Request:
Request URL: https://cornerpub.co.uk/api/orders/fb5331bf7d9e4dae90f25a190b16037f Accept: application/json Request Method: OPTIONS
- CORNER PUB: "Blimey! You sure are needy... **sighs** Here you go:"
-
Status Code: 200 Content Type: application/json Response: { "PUT":{ "description": "Add items to this order", "href": "/api/orders/fb5331bf7d9e4dae90f25a190b16037f", "params": { "itemType":{ "required":true, "type":"string", "description":"The menu type of the item to order" }, "itemId":{ "required":true, "type":"string", "description":"The id of the item to order" } } }, "PATCH"{ "description":"Update the order", "href": "/api/orders/fb5331bf7d9e4dae90f25a190b16037f", "params": { "closeOrder":{ "required":false, "type":"boolean", "description":"Use if closing an order" } } } "DELETE":{ "description": "Cancel this order", "href": "/api/orders/fb5331bf7d9e4dae90f25a190b16037f" } }
- THIRSTY DEVELOPER: "Thanks so much. I'll have the Kipling, then."
- HTTP Request:
Request URL: https://cornerpub.co.uk/api/orders/fb5331bf7d9e4dae90f25a190b16037f Accept: application/json Request Method: PUT Request Body: { "itemType":"beer", "itemId":"2743ce3ea2e04dbf972f8873a7f6e3c7" }
- CORNER PUB: [ The bartender's busy and sends a nod, signifying that he's received your order ]:
- Since the order cannot be fulfilled immediately, the server reponds with a status of
202
, saying that the order has been accepted and provides information on how to check the status of the order.Status Code: 202 Content Type: application/json Response: { "href":"/api/orders/fb5331bf7d9e4dae90f25a190b16037f/status", "estimatedWait":120000 }
- THIRSTY DEVELOPER (after the two minutes specified in the milliseconds wait time have passed): "Any update on my beer?"
I send aHEAD
request, to the Pub's API, which requires nothing to be returned but a status code:
HTTP Request:Request URL: https://cornerpub.co.uk/api/orders/fb5331bf7d9e4dae90f25a190b16037f/status Accept: application/json Request Method: HEAD
- CORNER PUB: "Sorry, mate. I had to tap a new keg. Your order is ready now:"
When we sent a status inquiry, the pub now replies with a code of205
, which tells us to reset our content and we're good to go.Status Code: 205 Content Type: application/json
- THIRSTY DEVELOPER: "Excellent!"
Now we reset our order and the the item has been delivered!
HTTP Request:Status Code: 200 Content Type: application/json Response: { "href":"/api/orders/fb5331bf7d9e4dae90f25a190b16037f", "recordset":{ "total": 1, "limit": 5 "order" : "timeAdded", "orderDirection": "asc", "sorted":[ "2743ce3ea2e04dbf972f8873a7f6e3c7" ], "href": { "next":"/drinks/beer?page=2" } }, "items":{ "2743ce3ea2e04dbf972f8873a7f6e3c7": { "name": "Kipling", "href": "/api/orders/fb5331bf7d9e4dae90f25a190b16037f/items/2743ce3ea2e04dbf972f8873a7f6e3c7" } } }
Obviously, this is a simplified example, but the idea of using a range of verbs and status codes to express the "conversation" between an API and its consumers could be expanded to include additional transactions, options. In our pub example, we could take additional steps to close the order out and request the check, find out options for ordering food, or add another beer ( we're thirsty! ).
ColdBox's built-in features for handling HTTP request verbs and data responses with expressive status codes make implementing rich "conversational" APIs painless, allowing you, the developer, to focus on the big-picture details of implementation, rather than the writing code to deal with the internals of how it will be accomplished. Happy coding!
Add Your Comment